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 at turntrout.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 at alexander-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 using imgcat, 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. With alt-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). The robots.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 with ssh for a few hours.
Fish shell with autocomplete, syntax highlighting, and the tide 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 via trash-put instead of rm,
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 the fish 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 the fortune command,
I pipe the saying into goosesay (my variant of the cow in the original cowsay),
The lolcat command splays the text ’cross the rainbow.
The original subfont 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 that only gives the agent what it needs to do your work. Tap-tap-tap glovebox 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 while pytest was crashing, because pytest | tee log exits with tee’s status—under a runCmd:/shell: sh/ custom bash that lacks pipefail.
check-exit-suppression
A teardown that left a volume pinned reported success, because cleanup || 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, because docker compose up 2>/dev/null threw away the only diagnostic.
check-substitution-exit-swallow
An allowlist-building loop reported success while adding nothing, because done < <(jq …) (or jq … | while read) discards jq/yq’s exit status—a renamed key or malformed input makes the producer exit non-zero, the loop iterates zero times, and the fail-open goes unnoticed. Curated to jq/yq (structured-data extractors whose non-zero exit is a fail-closed signal); opt out with # allow-substitution-exit: <reason>.
check-pr-paths
A required check hung at “Expected—Waiting” forever and the PR could never merge, because paths:/paths-ignore:/branches: on pull_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, because secret_store ls | grep -q "$name" under pipefail let grep’s early exit sigpipe the producer, surfacing 141 as no-match once the listing outgrew the pipe buffer.
The base image you reviewed and the one CI built diverged, because FROM node:22 is a mutable tag the registry can re-point. Demands a @sha256: digest.
check-pinned-downloads
A tampered release or compromised mirror swapped the binary you curled and then ran, because the download carried no checksum / signature check. Also fires on the one-line installer that never touches disk— curl -fsSL … | sudo sh—which pipes unverified bytes straight into a shell.
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 new always() 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 inline run: block shipped unchecked (unquoted expansions, missing pipefail) because shellcheck / shfmt / shellharden only see standalone .sh files.
check-concurrency
New pushes queued behind stale runs instead of cancelling, because a concurrency: block omitted cancel-in-progress and it silently defaulted to false.
check-static-concurrency
A required check hung at “Expected—Waiting” forever, because a static workflow-level concurrency.group (no github.ref/head_ref key) let a sibling ref’s run cancel this one’s pending run wholesale before any job—and its always() reporter—ever started.
check-requires-concurrency
A pull_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 inline run: for a policy marker (e.g. a history-rewrite command that demands fetch-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 its always() 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 one source 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 because ci-failure-notify.yaml’s on.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.
A tracked symlink with an absolute target (/Users/you/...) broke on every machine but the author’s.
check-unnamed-regex-groups
A RegEx’s match handling went positional and brittle because a re.* literal used an unnamed ( ) group.
check-global-stdio-swap
Concurrent calls clobbered each other’s output because code reassigned the process-global sys.stdout to capture I / O.
check-claude-model
A claude-code-action step billed Opus silently because it omitted --model and rode the action’s expensive default tier.
check-drift-guards
A copies-agree (“drift guard”) test shipped with no stated reason why a single source of truth is infeasible—the duplication it polices kept drifting anyway. Requires @pytest.mark.drift_guard("<why no SSOT is feasible>") on any Python test whose name / docstring reads as a drift guard, so the judgement is reviewed, not implied. JS / TS / shell suites (*.test.mjs, bash tests) carry the same guards without that decorator, so a sibling phrase pass covers them—annotate a flagged line with drift-guard-ok: <why no SSOT is feasible>.
check-graceful-handwave
A doc or comment claimed the code “fails gracefully”—a guarantee that specifies nothing (which input? which exit code?)—and nobody could tell whether the behaviour was real or wished-for. Scans prose (Markdown / rst) line-by-line and code comment-only; opt out by stating the behaviour: allow-graceful: <what actually happens>. Pass --prose to scan a free-standing text file (e.g. a PR body) line-by-line.
check-historical-comments
A comment narrating the past (“renamed from X”, “now uses Y”) rotted into a lie the moment the code moved—the reader can’t see the old code, so the note was unverifiable from day one. Bans only tokens with no present-tense reading; opt out (e.g. a reader of a legacy on-disk format) with # allow-history: <reason>.
check-doc-line-refs
A doc cited source by exact line number and pointed at whatever now happens to live there after the next refactor. Bans <file>.<ext>:<N> and (L<N>)-style cites in Markdown (fenced code blocks and any CHANGELOG.md are skipped); cite a function / section / anchor instead, or suppress with ``.
check-flag-arity
A CLI parser died with a raw $2: unbound variable instead of a clean “—branch needs a value,” because a --branch) X="$2"; shift 2 arm trusted the loop’s outer $# -gt 0 (which proves only $1) and the flag was passed last. Flags any case arm whose label is a -x/--xxx/--xxx=* option that reads $2/shift 2 without its own guard; the guard must both precede the read and actually bail ([[ $#-ge-2]] || die, a self-guarding ${2:?…}, or a need_val/need_arg helper)—a bare [[ $#-ge-2-1]] whose result is discarded, or a guard placed after the read, does not count. Suppress with # flag-arity-ok: <why>.
check-secret-file-perms
A credential file (*token*, *.pem, *npmrc*, …) was created world-readable with the default umask and only chmod 600’d on a later line, so a co-tenant could read it in the window between. Flags a secret-named create (>/>>, touch, tee, non-private install) that is tightened by a chmod 0?[46]00 on the same path within ~3 lines; a standing / inline umask 077 or install -m 600 is accepted, and a create with no nearby chmod is not flagged. Suppress with # secret-perms-ok: <reason>.
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 one warnings entry. 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, the a11y 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, calling url(#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-hatch as 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 new any 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.