Punctilio for meticulous typography

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,pre-commit hook, or⁠—for a zero-friction install in any project that already runs Prettier⁠—a Prettier plugin.

npm install punctilio

This website

I ’ve made 6,725 commits. That’s over halfway to being over 9,000!

This site is one of my most heartfelt works of art. I’ve passionately optimized its design while obsessively testing⁠—for example, 100% TypeScript branch coverage, 100% Python line coverage, and hundreds of visual regression tests.

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.

You can locally serve the site by running:

SITE_DIR=/tmp/TurnTrout.com
git clone https://github.com/alexander-turner/TurnTrout.com.git "$SITE_DIR" --depth 1
cd "$SITE_DIR"
yes | pnpm install --frozen-lockfile
pnpm dev

Automatic alt text generation

Install with  pip install alt-text-llm.

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.

A labeled diagram of the labeling pipeline.

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.

If you want to improve accessibility for your content, go ahead and check out my repository!

Protect datasets from scrapers

Install with  pip install easy-dataset-share.

I helped fund this project. Here’s the introduction to an article I wrote:

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.

Automated setup

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.

  1. Fish shell with autocomplete, syntax highlighting, and the  tide theme,
  2. neovim via LazyVim, providing a full ide experience,
  3. tmux with automatic session saving and restoration,
  4. envchain for hardware-encrypted secret management via macOS Secure Enclave or Linux gnome-keyring⁠—no more plaintext api keys in configuration files,
  5. Open source AI tool setup,
  6. autojump for quick directory navigation,
  7. Reversible file deletion by default via  trash-put instead of  rm,
  8. git aliases and other productivity shortcuts, and⁠—drum roll⁠—
  9. 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 6
end

The way this works is that:

  1. I sample a saying by calling the  fortune command,
  2. I pipe the saying into  goosesay (my variant of the cow in the original  cowsay),
  3. The  lolcat command splays the text ’cross the rainbow.

Faster font subsetting

@turntrout/subfont is a hard fork of  Munter/subfont, which shrinks fonts to only contain the characters necessary for the content.

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.

Claude Code automation template

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.

Sandbox your coding agent

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.

A researcher reaches through the sealed gloves of a laboratory glove box to work with material sealed inside, the barrier keeping it contained.

Disclaimer: I’m an AI professional but not a security professional. I welcome issues and PRs.

Make your CI confess

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 even though the real work failed⁠—a failing command’s exit code gets hidden by a pipe⁠—or a required check never reports at all and the PR hangs forever.
  • Identity lies: a base image or downloaded file is pinned to a mutable name (a tag, a bare url) that can change under you, so the bytes you run aren’t provably the bytes you reviewed.

What it checks

Honesty (Tier 1, default-on)

HookFailure it prevents
check-workflow-pipefailCatches a pipeline where a failing command’s exit code is hidden by the last command in the pipe. Example:  pytest | tee log reports  tee’s success even if  pytest crashed, so CI goes green. Fires when a  run: step uses a shell that doesn’t set  pipefail.
check-exit-suppressionCatches  cmd || true, which throws away a command’s failure but keeps its output, so a broken step still reports success. Example: a cleanup step that failed to release a resource looks like it worked.
check-stderr-suppressionCatches  cmd 2>/dev/null, which throws away error messages. When the command fails you get a bare non-zero exit and no clue why. Example:  docker compose up 2>/dev/null hides the reason the container wouldn’t start.
check-substitution-exit-swallowCatches a loop fed by  jq / yq whose exit code is ignored, so a parse error looks like “nothing to do.” Example:  done < <(jq …) or  jq … | while read⁠—if  jq chokes on a renamed key or bad input it exits non-zero, the loop runs zero times, and the failure goes unnoticed. Limited to  jq / yq. Opt out with  # allow-substitution-exit: <reason>.
check-pr-pathsCatches a required check that never runs and leaves the PR stuck at “Expected⁠—Waiting” forever, so it can never merge. A  paths:, paths-ignore:, or  branches: filter on  pull_request can skip the whole workflow silently. Example: a stacked PR whose base isn’t  main gets skipped by a branch filter.
check-pipefail-grep-pipeCatches  cmd | grep -q … under  pipefail: grep exits as soon as it finds a match, which kills the producer with sigpipe and surfaces as exit 141 (looks like “no match”). Example: a check that confirms a secret was removed wrongly reports it gone once the listing is large enough to fill the pipe buffer.

| check-folded-scalar-comment | Catches a line written as a  # comment inside a folded (> / >-) yaml block scalar. A folded scalar has no comment syntax, so the line is folded into the value⁠—and when that value is an argument string something shell-splits, the  # starts a shell comment and every argument after it is discarded. Example: a  claude_args: >- block gains an explanatory comment, and the  --allowedTools / --add-dir flags below it never reach the CLI⁠—the diff reads as though the scoping were in force while it is absent. Only judges a folded block that also carries a  --leading line (an argument string, not prose); literal  | scalars keep newlines and are out of scope. Opt out with  # allow-folded-scalar-comment: <reason>. |

| check-gh-slurp-jq | Catches a  gh api call  gh refuses to run:  --slurp alongside  --jq / --template, or  --slurp with no  --paginate. Both are rejected while  gh parses its arguments, before any request goes out, so the call exits non-zero on every run⁠—it has never worked. Worse than a loud bug: a site that swallows the failure reads as a permanent green, and one that doesn’t reds a scheduled job 100% of the time. Example: a pagination helper written with  --slurp --jq '.[]' that has silently returned nothing since the day it was added. Reads backslash-continued calls whole, so  --jq on a later line still counts. Opt out with  # allow-gh-slurp-jq: <reason>. |

Identity (Tier 1, default-on)

HookFailure it prevents
check-pinned-base-imagesCatches a Docker base image pinned to a tag the registry can quietly re-point to different bytes. Example:  FROM node:22 today may not be the same image tomorrow. Requires a  @sha256: digest so the image you reviewed is the image CI builds.
check-pinned-downloadsCatches downloading a binary and running it with no checksum or signature check, so a tampered release or hacked mirror can swap it. Also flags one-line installers like  curl -fsSL … | sudo sh, which pipe unverified bytes straight into a shell.
check-provenance-repo-urlCatches a  package.json (or  pyproject.toml) whose repository url still points at the repo it was forked from. Example: a fork’s first  npm publish --provenance fails with  E422 … Failed to validate repository information because the url names the upstream, not this fork. Compares the declared repository url against your  origin remote (never  Homepage). A mismatch has no opt-out⁠—forks must fix their url. Repos with no  origin remote are skipped.

Security (Tier 1, default-on)

HookFailure it prevents
check-trusted-baseCatches the “pwn-request” hole, where a  pull_request or  pull_request_target job checks out the PR’s own code and runs with write permissions or secrets. Example: an outside contributor’s code then runs with your repo’s credentials and can steal your secrets. Also catches the same job checking out an author-chosen base ref⁠—a PR’s base is a branch the author picks, so a job that stages “the base branch” can be pointed at a branch carrying a rewritten build script while the PR’s own diff stays clean. Narrowing the base to `main
check-untrusted-execCatches the same job actually executing the checked-out pull-request code while secrets are live⁠—a local composite action (uses: ./…, whose manifest GitHub reads from the workspace), a package-manager script (pnpm build, npm run test, make release, npx …, whose body comes from the checked-out  package.json / Makefile), or a workspace-relative path (bash ./scripts/x.sh). Example: the PR rewrites  package.json’s  build script, and your release pat is in the environment when it runs. Reaches jobs  check-trusted-base cannot see⁠—a matrix-derived head ref, an unpinned  workflow_run head⁠—and, deliberately, a job whose  # trusted-base-ok reason claims it “only runs the trusted base copy” while the entrypoint itself still comes out of the workspace. Staging scripts into  $RUNNER_TEMP is not a defence: an earlier attacker step in the same job owns  $GITHUB_ENV / $GITHUB_PATH and that directory. Opt out with  # untrusted-exec-ok: <reason> in the job block.

| check-unscoped-tool-grant | Catches a Claude Code tool grant (--allowedTools, an  *ALLOWED_TOOLS env var, an  allowed_tools: input) that is unscoped or inert. A file tool named with no path⁠—Read, Grep, Glob, Write, Edit⁠—is a whole-tool grant applied after the per-path working-directory check and overriding its verdict, so an  --add-dir beside it suppresses a prompt rather than building a jail. And a path rule spelled under any name but  Read or  Edit is a rule nothing consults: Write(//out.json) denies the write it appears to grant (Edit(...) is the spelling that confines Edit, Write, MultiEdit and NotebookEdit; an absolute path needs two leading slashes). Established by probing the real CLI headless against a stub api, each case against a matched denying control⁠—undocumented internals, so the docstring names the version and tells you to re-probe on a much newer CLI. Opt out of the unscoped half with  # allow-unscoped-read-grant: <reason> / # allow-unscoped-write-grant: <reason>⁠—worth it when the same job grants bare  Bash, since a redirect writes anything regardless. The inert half has no opt-out: a rule nothing consults is never the right thing to write. |

Opinionated (Tier 2, opt-in)

HookFailure it prevents
check-always-reporterCatches a required check left stuck at “Expected⁠—Waiting” when a gate job skips all the real work jobs. Assumes you use a decide-job plus an  always() reporter job.
check-required-reporterCatches a new  always() reporter that is green but was never added to the branch-protection required list, so it doesn’t actually gate anything. Assumes the required list is mirrored from these annotations.
check-job-timeoutCatches a job with no  timeout-minutes, which inherits GitHub’s 360-⁠minute default. Example: a hung test or a network fetch with no deadline can hold a shared runner for six hours. Requires every job to set its own limit; jobs that call a reusable workflow (uses:) are exempt. Opt out with  # allow-no-timeout: <reason>.
check-inline-run-lengthCatches long inline  run: scripts that ship unchecked, because shellcheck / shfmt / shellharden only inspect standalone  .sh files, not inline blocks. A long inline script can hide bugs like unquoted variables or a missing  pipefail.
check-concurrencyCatches a  concurrency: block missing  cancel-in-progress, which defaults to  false. New pushes then queue behind stale runs instead of cancelling them.
check-static-concurrencyCatches a workflow-level  concurrency.group with no per-ref key (no  github.ref / head_ref). A run from another branch can then cancel this one’s pending run before any job starts, so its  always() reporter never fires and a required check hangs forever.
check-pending-cancel-concurrencyCatches a config that turns a required check red for no real reason. If  on.pull_request.types includes types beyond opened / synchronize / reopened (e.g.  labeled⁠—a Dependabot PR is born with labels), several runs queue on the same commit; a per-PR  concurrency.group then lets GitHub cancel one same-commit run, and its  always() reporter reports “cancelled” (a red X with no actual failure).  cancel-in-progress can’t help. Fix: drop the group or key it on  github.run_id. Opt out with  # pending-cancel-ok.
check-requires-concurrencyCatches a  pull_request / pull_request_target workflow with no concurrency: block at all, so every push to a PR starts a second full run instead of cancelling the old one⁠—wasting a limited runner pool. (check-concurrency only checks a block that exists; this one requires the block.) Satisfied by a block at the workflow or job level. Opt out with  # concurrency-not-required.
check-externalized-markersCatches a workflow guard that scans inline  run: for a required marker but goes blind when that command moves into a  .github/scripts/*.sh file or a composite action. Example: a guard that requires  fetch-depth: 0 whenever a history-rewrite command runs stops seeing the command once it’s in a script, and passes even though the requirement is now unmet.
check-path-gate-depsCatches a gated job that skips⁠—and its  always() reporter goes green⁠—on the exact PR that changed a file the job needs, because the decide job’s path filters left out a composite action or  .github/scripts/ helper. Checks that every gated job’s dependencies (composites, sourced scripts one hop deep, and  # gate-deps:-declared paths) are covered by the filters. Suppress one dependency with  # path-gate-ok: <dep> <reason>.
check-failure-notifier-coverageCatches a new push / schedule workflow that fails silently because it was never added to the failure notifier’s  on.workflow_run.workflows list (workflow_run has no wildcard, so the list is hand-maintained). The notifier is found by shape⁠—a workflow triggered  on.workflow_run that names a notification sink⁠—never by filename, and finding none is a failure, not a silent pass. Every listed name must match a real workflow, and the list must cover every push / schedule workflow whose failure reaches nobody else⁠—not the whole tree: one that self-notifies on failure, that also runs on  pull_request (its red is already a check on the PR), or that carries a reasoned  # cron-alert: false is somebody else’s responsibility, and demanding it here would double-page or overrule the opt-out. Watching more than that minimum is allowed. Prints the corrected block on mismatch.  --allow-no-notifier passes when no notifier exists at all;  --notifier FILENAME names the file the fail-closed message should suggest (default  ci-failure-notify.yaml); --notifier-pattern REGEX (repeatable) teaches it a house sink;  --require-alert-priority also fails a step calling a local composite action that defaults  priority: without stating one, since an omitted priority silently means that action’s maximum.
check-token-fallbackCatches a token that silently switches identity, like  token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}. The day someone sets  PAT, pushes start using a different identity⁠—which can break permissions in surprising ways. Flags any  secrets.A || secrets.B in a token position (a  token: / github-token: input, or a  GITHUB_TOKEN / GH_TOKEN env var). Opt out with  # token-fallback-ok: <reason> when the switch is intentional.
check-workflow-secret-namesCatches a misspelled or renamed secret reference. A wrong  secrets.* name just evaluates to empty, so the feature quietly degrades with no error. Example: reading  secrets.ANTHROPIC_API_KEY when the real secret is  GH_ACTION_ANTHROPIC_API_KEY. Requires every  secrets.* / vars.* name under  .github/ to match the checked-in  .github/workflow-secrets.txt allowlist (GITHUB_TOKEN is implicit). Prints the corrected file on mismatch.
check-pin-comment-truthCatches wrong or inconsistent version comments on sha-pinned actions⁠—the comment is the only human-readable part of a pin. Example: the same  actions/checkout@<sha> is labeled  # v6 in one place and  # v7.0.0 in another, so at most one is true. Requires every sha-pinned  uses: to carry a well-formed  # v<number> comment, and one sha to use one comment string across the repo. No network lookups. Opt out with  # pin-comment-ok.
check-stderr-merge-parseCatches parsing a stream that merged stderr into stdout with  2>&1, where a warning line can be mistaken for real output. Example: an npm warning becomes “the version” and every release aborts on the bogus value. Flags a  2>&1 capture piped into a parser (head / tail / grep / awk / cut / sed / jq / sort / wc) or used in a  [[ … ]] / (( … )) comparison. A plain  out=$(cmd 2>&1) used only for  echo / printf (diagnostics) is fine. Opt out with  # stderr-merge-ok: <reason>.
check-echo-fallbackCatches  $(cmd || echo "…"), which turns a failure into a normal-looking value that later code trusts. Example: a release step reads the literal string  error as the version. Flags  || echo / || printf inside command substitutions and as bare statements. A fallback that writes to stderr and aborts (|| { echo … >&2; exit 1; }) is real recovery and passes. Opt out with  # echo-fallback-ok: <reason>.
check-lockstep-pinsCatches two pins that are supposed to stay in sync but are linked only by a comment. Example: a  .pre-commit-config.yaml rev: and a workflow’s  pip install git+…@<sha> that must name the same release. Config-driven: each  --pair FILE1 REGEX1 FILE2 REGEX2 (one capture group each) must match exactly once per file and the two captures must be equal⁠—zero or multiple matches is a hard error. Not in the  check-tier2 aggregate (it needs per-repo args); enable it on its own.

Unrelated bonus checks (Extras)

HookFailure it prevents
check-symlinksCatches a committed symlink pointing at an absolute path (/Users/you/...), which works only on the author’s machine and breaks everywhere else.
check-unnamed-regex-groupsCatches a  re.* pattern that uses a plain  ( ) group, which forces brittle position-based match handling. Named groups  (?P<name>…) are clearer and survive edits.
check-global-stdio-swapCatches code that reassigns the process-global  sys.stdout to capture output. Under concurrency, calls then clobber each other’s output.
check-claude-modelCatches a  claude-code-action step with no  --model, which rides the action’s expensive default tier and can bill for Opus without you meaning to.
check-drift-guardsCatches a “these copies must agree” (drift-guard) test that never says why a single source of truth isn’t feasible⁠—so the duplication it polices keeps drifting anyway. Requires  @pytest.mark.drift_guard("<why no SSOT is feasible>") on any Python test that reads as a drift guard, so the judgement is reviewed, not implied. JS / TS / shell suites (*.test.mjs, bash tests) can’t use the decorator, so a phrase check covers them⁠—annotate a flagged line with  drift-guard-ok: <why no SSOT is feasible>.
check-graceful-handwaveCatches vague claims like “fails gracefully” in docs or comments⁠—they promise nothing (which input? which exit code?), so no one can tell if the behaviour is real. Scans prose (Markdown / rst) line by line and code comments only. Opt out by stating what actually happens:  allow-graceful: <what happens>. Pass  --prose to scan a plain text file (e.g. a PR body) line by line.
check-historical-commentsCatches a comment describing the past (“renamed from X”, “now uses Y”). The reader can’t see the old code, so the note was never verifiable and rots into a lie. Bans only words with no present-tense reading. Opt out (e.g. a reader of a legacy on-disk format) with  # allow-history: <reason>.
check-doc-line-refsCatches a doc that cites source by line number, which points at whatever happens to live there after the next refactor. Bans  <file>.<ext>:<N> and  (L<N>) cites in Markdown (fenced code blocks and any  CHANGELOG.md are skipped). Cite a function, section, or anchor instead, or suppress with ``.
check-flag-arityCatches a CLI flag that reads its value without checking one was passed, so it dies with  $2: unbound variable instead of a clean “⁠—branch needs a value.” Example: a  --branch) X="$2"; shift 2 arm passed as the last argument. Flags any  case arm for a  -x / --xxx / --xxx=* option that reads  $2 / shift 2 with no guard that both comes first and actually bails ([[ $#-ge-2]] || die, ${2:?…}, or a  need_val / need_arg helper). A bare  [[ $#-ge-2-1]] whose result is discarded, or a guard after the read, doesn’t count. Suppress with  # flag-arity-ok: <why>.
check-secret-file-permsCatches a credential file (*token*, *.pem, *npmrc*, …) created world-readable and only  chmod 600’d a few lines later⁠—a co-tenant can read it in that window. Flags a secret-named create (> / >>, touch, tee, non-private  install) tightened by a later  chmod 0?[46]00 on the same path within ~3 lines. A  umask 077 or  install -m 600 up front is accepted, and a create with no nearby chmod isn’t flagged. Suppress with  # secret-perms-ok: <reason>.
check-case-defaultCatches a shell  case with no default arm, so an unexpected value matches nothing and the script runs on. Example: an unknown bump-type leaves  NEW_VERSION unset and the release continues on garbage. Requires a bare  *) (or  (*) / * ) / a  x|*) alternative) default arm on every  case … esac; globs like  *.txt) or  --*) don’t count. Opt out with  # case-default-ok: <reason> on the  case line.
check-cron-commentCatches a schedule comment that contradicts its cron. Example: the comment says “daily” but the cron runs weekly, so the job runs 1/7th as often as everyone thinks. Pairs a cadence word (hourly / daily / weekly / monthly / every N minutes|hours|days) in a comment on or within 3 lines above a  cron: line with the expression, and fails only on a clear contradiction⁠—lists, ranges, and exotic crons always pass. Opt out with  # cron-comment-ok.
check-cron-alert-coverageCatches a scheduled workflow whose failures reach nobody. A cron has no PR surface⁠—no check run, no reviewer⁠—so its red lands in an Actions tab nobody opens and a guard that has been failing for six weeks looks exactly like one that has been passing. By default flags only the two lies: a malformed  # cron-alert: marker, and a notification step sitting behind a success-only gate (if: success(), a job gated  needs.x.result == 'success'), which reads as coverage in review and fires exactly never. Pass  --require-alert to also demand every scheduled workflow route failures to a human⁠—by a step gated  if: failure() / .result != 'success', or by a failure notifier in the tree listing it in  on.workflow_run.workflows (the same predicate  check-failure-notifier-coverage uses, so the two never demand opposite things of one workflow). A PR trigger does not count here: a cron fire has no pull request.  --notifier-pattern REGEX (repeatable) teaches it a house sink on top of the built-in ones (ntfy, Slack, PagerDuty,  gh issue create, …). Opt out with  # cron-alert: false # <reason> on the  schedule: (or  push:) key.
check-cron-staleness-optoutCatches the cron that produces no failure because it produces nothing⁠—GitHub disables a dormant repo’s schedules after 60 days, and a  startup_failure / skipped / cancelled run is not a failed run, so a failure notifier stays correctly silent while the guard has been dead for two months. Enforces the  # cron-stale: false # <reason> marker discipline (# cron-alert: false deliberately does not satisfy it: its reason argues a failed fire self-heals next cycle, which presumes there is a next cycle).  --watchdog-workflow FILENAME names the runtime watcher and requires that file to exist under  .github/workflows/ and itself be schedule-triggered;  --require-stale-marker demands a marker on every scheduled workflow, narrowed to the watchdog alone when a valid one is declared. Without flags only a present-but-malformed marker is reported.
check-toolchain-skipsCatches a test that skips itself when a tool is missing (skipif(shutil.which("node") is None)), which silently drops all coverage of the guarded scripts on a runner without that tool while the suite stays green. Flags  pytest.mark.skipif / pytest.importorskip conditions that probe for a binary (shutil.which, which(, find_executable) with no CI guard⁠—in CI the skip must instead fail: shutil.which("node") is None and not os.environ.get("CI"). Only test files are scanned. Opt out with  # toolchain-skip-ok: <reason>.
check-env-symmetryCatches a half-finished env-var rename: the var was changed where it’s set but not where it’s read (or vice-versa), so the reader just sees an unset value and falls back to a default. Scans the whole tracked tree for every var matching a  --prefix (e.g.  GLOVEBOX_) and flags any that is written-but-never-read or read-but-never-written. Dynamically-built names are skipped; an out-of-band var opts out with  # env-symmetry-ok: <VARNAME> <reason>. Needs  args: [--prefix, <PREFIX>]; not part of a tier aggregate.
check-test-predicate-shadowCatches a shell test that redefines a production pure predicate⁠—a function whose body only tests and returns. Stubbing a dependency (a logger,  as_root, a network call) is legitimate: there is a side effect to intercept. A predicate has none, so redefining one substitutes the test’s own copy of the logic and asserts against that copy⁠—usually a weaker one: a  valid_host_port stub matching  ^[0-9]+$ accepts the leading-zero ports the shipped  ^[1-9][0-9]{0,4}$ exists to reject, so the regression it covers can never fail it. Purity is decided on the real bash grammar, so anything richer than a flat  [[ … ]] / (( … )) / return / true / false chain reads as impure and a dependency stub is never flagged. Source the real definition, or opt out with  # predicate-shadow-ok: <reason>. A violation is a pair, so the passed file list only ever narrows the scan: the production side always self-discovers via  git ls-files, and a passed production file widens the test side to the whole tree.
check-stray-tool-markupCatches an agent’s leaked file-authoring scaffolding⁠—a bare closing  content / invoke tag (or an  antml:-prefixed variant) committed onto its own line in a doc, where it renders as literal garbage that only a human caught. Flags a line that is entirely a stray tool-call tag (</invoke>, an opening  invoke / parameter tag,  <function_calls>, a bare closing  content tag); inline mentions, inline-code spans, and fenced code blocks are never flagged. Suppress a genuine case with  allow-stray-markup: <reason> on the line above.

Sanitize untrusted text before your agent sees it

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.

npm install agent-input-sanitizer

Quick start

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.

Pull requests on other projects

Mermaid diagrams now generate unique element IDs

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.

Jest  --collect-only flag

Jest lacked a way to enumerate test cases without running them, unlike  pytest --collect-only. PR 16,006 and PR 16,259 fixed that.

KaTeX TypeScript cleanup

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.

Scss linting rule

I contributed a rule to stylelint-scss. I ran into the following issue:

  1. I defined a css --property.
  2. I defined the  --property using the scss variable  $var.
  3. In this specific context, browsers will not interpolate  $var which means the final css contains the literal “$var.”

To fix the problem,  $var must be interpolated into  #{$var}. My  custom-property-no-missing-interpolation rule catches and automatically fixes this mistake.

Find out when I post more content: newsletter & rss

Thoughts? Email me at  alex@turntrout.com (pgp)

Similar posts