punctilio (n.): precise observance of formalities.
Pretty good at making your text pretty. The most feature-complete and reliable English typography package.punctilio transforms plain ascii into typographically correct Unicode, even across html element boundaries. Try it live atturntrout.com/punctilio.
Smart quotes · Em / en dashes · Ellipses · Math symbols · Legal symbols · Arrows · Primes · Fractions · Superscripts · Ligatures · Non-breaking spaces · html-aware · Markdown support · Bri’ish, German, and French localisation support
import { transform } from "punctilio";transform(`"It's a beautiful thing, the destruction of words..." -- 1984`);// → “It’s a beautiful thing, the destruction of words…”—1984
punctilio accepts three input formats: text, Markdown, and html. Use it as a library, a CLI, a pre-commit hook, or—for a zero-friction install in any project that already runs Prettier—a Prettier plugin.
I open source my website infrastructure and article edit histories atalexander-turner/TurnTrout.com. I license the repository under CC by-sa 4.0, which means you can share and adapt the site as long as you provide attribution and distribute any derivative works under the same license.
When I started writing in 2018, I didn’t include alt text. Over the years, over 500 un-alt’ed images piled up. These (mostly) aren’t simple images of geese or sunsets. Most of my images are technical, from graphs of experimental results to hand-drawn AI alignment comics. Describing these assets was a major slog, so I turned to automation.
To implement accessibility best practices, I needed alt text that didn’t describe the image so much as communicate the information the image is supposed to communicate. None of the scattershot AI projects I found met the bar, so I wrote my own package.
alt-text-llm is an AI-powered tool for generating and managing alt text in Markdown files. Originally developed for this website,alt-text-llm streamlines the process of making web content accessible. The package detects assets missing alt text, suggests context-aware descriptions, and provides an interactive reviewing interface in the terminal.
Generating alt text for maze diagrams from Understanding and Controlling a Maze-solving Policy Network.alt-text-llm displays the surrounding text (above the image), the image itself in the terminal usingimgcat, and the llm-generated alt suggestion. The user interactively edits or approves the text.
In the end, I generated over 550 high-quality alt-text suggestions for about $12.50 using Gemini 2.5 Pro. Withalt-text-llm, I addressed hundreds and hundreds of alt-less images: detecting them; describing them; reviewing them; and lastly applying my finalized alts to the original Markdown files.turntrout.com is now friendlier to the millions of people who browse the web with the help of screen readers.
Dataset contamination is bad for several reasons. Most obviously, when benchmarks are included in AI training data, those benchmarks no longer measure generalization—the AI may have been directly taught the answers. Even more concerningly, if your data promote negative “stereotypes” about AIs, they might become self-fulfilling prophecies, training future models to exhibit those same behaviors.
In the Claude 4 system card, Anthropic revealed that approximately 250,000 transcripts from their alignment faking paper had been scraped from the public web and included in their pretraining data. This caused an early model to hallucinate details from the paper’s fictional scenarios, forcing Anthropic to implement unique mitigations. Speculatively, this kind of misalignment data could degrade the alignment of any models trained thereafter.1
Data scraping practices are a serious problem. The tool we are currently releasing will not stop state-of-the-art actors. Since I wanted to at least mitigate the problem, I put out a bounty for a simple, open source tool to harden data against scraping. The tool is now ready:easy-dataset-share. In less than 30 minutes and at a cost of $0, you can deploy a download portal with basic protections against scrapers, serving a canary-tagged dataset with modest protections against AI training.
easy-dataset-share will not stop sophisticated scrapers
Sophisticated scraping operations can bypass Cloudflare Turnstile for about $0.001 cents per trial (via e.g. CapSolver). Therobots.txt and Terms of Service are not technically binding and rely on the good faith of the user, although the ToS does provide limited legal deterrence. Canary strings can be stripped from documents. Overall, this tool is just a first step towards mitigating dataset contamination. We later discuss improvements which might protect against sophisticated actors.
One command to set up your shell, editor, and secret management.
My.dotfiles repository provides comprehensive development environment setup. With this command, I quickly personalize any shell—even if I’m just visiting withssh for a few hours.
Fish shell with autocomplete, syntax highlighting, and thetide theme,
neovim via LazyVim, providing a full ide experience,
tmux with automatic session saving and restoration,
envchain for hardware-encrypted secret management via macOS Secure Enclave or Linux gnome-keyring—no more plaintext api keys in configuration files,
Open source AI tool setup,
autojump for quick directory navigation,
Reversible file deletion by default viatrash-put instead ofrm,
git aliases and other productivity shortcuts, and—drum roll—
goosesay, because every terminal needs more geese.
______________________________________ / Find out just what any people will \ | quietly submit to and you have the | | exact measure of the injustice and | | wrong which will be imposed on them. | \ --- Frederick Douglass / -------------------------------------- \ \ \ ___ .´ ""-⹁ _.-´) e _ '⹁ '-===.<_.-´ '⹁ \ \ \ ; \ ; \ _ | '⹁__..--"" ""-._ _.´) / ""-´ _> : -´/ ; .__< __) \ '._ .__.-' .-´ '⹁_ '-⹁__.-´ / '-⹁__/ ⹁ _.´ ____< /'⹁__/_."" .´.----´ | | .´ / | | ´´-/ ___| ; <_ / `.'´
Each time I open thefish shell, a rainbow goose blurts out an interesting phrase. I spent several hours to achieve this modern luxury.
if status is-interactive fortune 5% computers 5% linuxcookie 2% startrek 88% wisdom | cowsay -f ~/.dotfiles/apps/goose.cow | lolcat -S 6end
The way this works is that:
I sample a saying by calling thefortune command,
I pipe the saying intogoosesay (my variant of the cow in the originalcowsay),
Thelolcat command splays the text ’cross the rainbow.
The originalsubfont traced font usage from scratch on every page. That took almost two hours per deploy. My fork groups pages by their css and only traces one representative per group, extracting just the text from the rest (for my site: 382 ⭢ 5 css traces). Those remaining traces run in parallel across worker threads.
alexander-turner/claude-automation-template packages my automation workflows into a reusable starting point for any project using Claude Code. The template is designed so that adopting repos get improvements automatically via the sync workflow—fix a bug in the template, and every downstream project picks it up.
When handling dangerous materials, a chemist doesn’t yolo it barehanded on the open bench. They reach through a glovebox to do their work.
Right now, you’re probably barehanding AI, tossing it into a shell onto your machine with access to your credentials. At most, you’re shielded by a single prompted “auto-mode” classifier. Doing better is hard and while there are a few papers on best practices, it’s not clear how to do it properly and you have experiments to run.
After all, what’s the chance that something bad happens?
glovebox is a sealed enclosure which only give the agent what it needs to do your work. Tap-tap-tapglovebox and press Enter to spin up a hardware-isolated, allowlist-firewalled microVM, employing input / output sanitization to strip injections and tricks the AI might play on you, with an AI monitor with a red-alert ability to push-notify your phone and halt the AI until you return. The goal is a minimal-friction secure experience that gets the job done.
Disclaimer: I’m an AI professional but not a security professional. I welcome issues and PRs.
Make your CI confess what it’s hiding. A pack of fast, offline pre-commit
lints that catch two kinds of lie a green check can hide:
Honesty lies: the pipeline reports success while the real work failed
(exit codes masked by pipes), or a required check silently never reports and
the PR hangs forever.
Identity lies: a base image or downloaded artifact is pinned to a
mutable name (a tag, a bare url), so the bytes you run aren’t provably the
bytes you reviewed.
CI went green whilepytest was crashing, becausepytest | tee log exits withtee’s status—under arunCmd: / shell: sh / custombash that lackspipefail.
check-exit-suppression
A teardown that left a volume pinned reported success, becausecleanup || true discarded its non-zero exit while keeping its output.
check-stderr-suppression
A container launch failed with a bare non-zero and no clue why, becausedocker compose up 2>/dev/null threw away the only diagnostic.
check-pr-paths
A required check hung at “Expected—Waiting” forever and the PR could never merge, becausepaths: / paths-ignore: / branches:onpull_request skipped the workflow without reporting (a stacked PR on a non-main base is the branch-filter trap).
check-pipefail-grep-pipe
A teardown check reported a still-present secret as removed, becausesecret_store ls | grep -q "$name"underpipefail let grep’s early exit sigpipe the producer, surfacing 141 as no-match once the listing outgrew the pipe buffer.
A gated workflow stranded a required check at “Expected—Waiting” when the decide gate skipped every work job. Assumes a decide-job + always() reporter pattern.
check-required-reporter
A newalways() reporter shipped as a green-but-never-required check because nothing tied a workflow’s reporters to the branch-protection required-set. Assumes the required-set is mirrored from these annotations.
check-inline-run-length
A long inlinerun: block shipped unchecked (unquoted expansions, missingpipefail) because shellcheck / shfmt / shellharden only see standalone.sh files.
check-concurrency
New pushes queued behind stale runs instead of cancelling, because aconcurrency: block omittedcancel-in-progress and it silently defaulted tofalse.
check-static-concurrency
A required check hung at “Expected—Waiting” forever, because a static workflow-levelconcurrency.group(nogithub.ref / head_ref key) let a sibling ref’s run cancel this one’s pending run wholesale before any job—and itsalways() reporter—ever started.
check-requires-concurrency
Apull_request(_target) workflow shipped with noconcurrency: block at all, so every push to a PR started a second full run instead of cancelling the superseded one—stacking runs on a capped, shared runner pool. (check-concurrency only validates a block that exists; this one requires it to exist.) Satisfied by a block at the workflow level or on any job. Opt out with# concurrency-not-required.
check-externalized-markers
A workflow guard that scans inlinerun: for a policy marker (e.g. a history-rewrite command that demandsfetch-depth: 0) went blind and passed vacuously the moment that command moved into.github/scripts/*.sh or a composite action. Flags any job where the marker is reachable only through that indirection.
check-path-gate-deps
A gated job silently skipped—and itsalways() reporter went green—on the exact PR that changed a file the job depends on, because the decide job’s path filters omitted a composite action or.github/scripts/ helper. Verifies every gated job’s static dependencies (composites, run scripts onesource hop deep, and# gate-deps:-declared paths) are covered by the decide filters; suppress one dep with# path-gate-ok: <dep> <reason>.
check-failure-notifier-coverage
A new push / schedule workflow failed silently forever becauseci-failure-notify.yaml’son.workflow_run.workflows list (necessarily a hand-copied list—workflow_run has no wildcard) was never updated. Round-trip freshness check: the list must equal the tree’s push / schedule workflow names; prints the corrected block on mismatch. Pass--require-notifier to fail when the notifier workflow itself is missing.
Most prompt-injection tools run a classifier over the text and hope it
generalizes. This library targets a narrower, verifiable claim: the
specific byte-level channels—invisible Unicode, ansi escapes, human-hidden
Html, confusable glyphs, exfil-shaped urls—that let an attacker smuggle a
payload the operator can’t see but the model still reads. Every layer is a
deterministic transform you can unit-test with equality assertions.
import { sanitize } from "agent-input-sanitizer";// Layer 1 (invisible chars + ANSI), zero heavy deps:const { cleaned, found, warnings } = await sanitize(untrustedText);// Opt into the HTML layers for web ingress (lazy-loads ~200 ms of deps):const result = await sanitize(pageSource, { html: true });
sanitize never throws and never silently drops content—any change comes with
at least onewarningsentry.found names the neutralized category codes
(e.g.["cf-format", "hidden-html"]); cleaned is the safe text, with
placeholders where hidden html was spliced out.
My site uses Mermaid diagrams for compact, searchable, and dynamically styled graphics. However, thea11y accessibility checker revealed a “bigly” problem. When a page contains multiple diagrams, some element IDs collide (like those for arrowhead markers and node containers). Thus, callingurl(#arrowhead) in the third diagram might bind to a marker defined in the first. Arrowheads vanish, click handlers fire on the wrong diagram, and css styles corrupt.
The issue had been reported repeatedly since 2020 (#1,318,#3,267,#3,433,#4,346) with no comprehensive fix in sight. In February 2026, I aimed Claude Code at this problem. My PR 7,410 prefixed every element ID with its diagram’s unique svg container ID, making collisions impossible. I also ensured that all future diagram types generate unique IDs across diagrams.
KaTeX’s TypeScript codebase carried dozens of escape-hatchas any casts left over from its earlier Flow-to-TypeScript migration. Each one silenced a type error instead of fixing it, so the compiler couldn’t catch bugs in those spots. PR 4,171 replaced these with proper checks and tighter type definitions, then enables a lint rule so newany casts can’t sneak back in.
About MeI’m TurnTrout, but the United States government insists on calling me “Alexander Matt Turner.” I like writing and learning about lots of stuff.