Hello there!

This is a site for stuff I write that doesn’t fit in 280 characters or simple images.

My security camera tells me when the laundry is done

I wish Reolink did more with the speakers in its cameras. They’re already powered, on the network, and sitting in useful places. They could double as little smart speakers for announcements, but that use doesn’t seem to get much attention.

I got these camera announcements working with the assistance of Codex. I supplied the ideas and directed the work; Codex did the technical investigation and implementation for my integration fixes and automations. The existing integrations are credited below. Codex also drafted this post.

I’d already opened an issue about adding speaker playback to the library behind Home Assistant’s official Reolink integration. I decided to have Codex investigate getting it working for my setup.

I wanted to know when the washer or dryer finished, even from across the house with the laundry-room door closed. There was already a Reolink camera with a speaker next to the stairs, near the bedrooms and a bit away from the machines. That seemed like a useful place for the announcements to come from.

First I needed Home Assistant to know when the appliances finished. Samsung had announced a $4.99/month plan for SmartThings API access, with free access being phased out starting in October. I wasn’t about to pay a stupid subscription for that. I used LocalThings instead: a free, open source integration that talks to the washer and dryer directly over the local network.

I still had to connect the appliances to SmartThings first to get LAN access working, and keep them registered there. LocalThings handles Home Assistant’s ongoing communication with them locally.

The official Reolink integration still lists two-way audio and text-to-speech as unavailable, so I used a custom integration.

I started with ha-reolink-talk, Matthew Holm’s integration based on joeblack2k’s Reolink Talk. It already provides camera media players for audio files and text-to-speech, including cameras behind an NVR. I had Codex investigate and fix a couple of problems with my setup in a fork.

The first was camera discovery. Codex found that a separate request to the NVR failed because of its self-signed HTTPS certificate. The error was caught, and discovery quietly fell back to channel 0. I’m still wondering how the hell this worked in the upstream author’s setup without hitting that certificate error. Meanwhile, Home Assistant’s official Reolink integration was already connected and knew which channels existed. Codex changed discovery to reuse that information, which got the right cameras to show up.

The audio framing needed fixing too. The encoded audio blocks weren’t being packaged the way the camera expected. Codex corrected the block metadata, padding, and stream framing, which got speech playing through the camera. Those fixes are in my fork and an upstream pull request.

Once playback worked, I had Codex implement the laundry automations. Each appliance’s LocalThings progress sensor reaching finish triggers an announcement through the camera by the stairs. Piper supplies the speech: “The washer has finished its cycle,” or the dryer equivalent.

I had Codex put together a few versions of the sound for me to try, including repeated flute announcements and a Samsung completion melody. With the appliance and the camera playing the Samsung melody out of sync, it was a confusing cacophony. I went with one short tone followed by speech instead. The washer gets a higher tone and the dryer a lower one. Codex rendered each tone and its spoken message together into one audio file, so the camera only has to start playback once.

The laundry automations are specific to this house, but the integration fixes are there for other Reolink owners to use. A camera speaker is a pretty convenient little announcement speaker when it’s already in the right place.

12:00 am / Home assistant , Reolink , Automation , Ai assisted , Codex

My wobbly VHS-Decode pipeline

I wanted to recover some VHS tapes. Somehow this turned into tapping a signal inside a Sony VCR, putting capture cards in a computer from the junk pile at work, spreading the processing across three machines, and asking Codex to help redesign an amplifier PCB.

I built this VHS pipeline with the assistance of Codex. I supplied the ideas, directed the work, and handled the physical hardware; Codex did essentially all of the technical investigation and custom coding for my setup. The pipeline builds on the existing projects credited below. Codex also drafted this post.

The usual way to digitize VHS is to connect a VCR’s video and audio outputs to a capture device and record them on a computer. The VCR reads the tape and turns its signal into a picture before the computer ever sees it.

For better results, you can look for a nicer VCR with a built-in time base corrector, or TBC, which helps correct playback timing errors that make the picture wobble. Unfortunately, the decks I was looking at were $$$. With ancient media having a moment again, it feels like everybody wants the same aging hardware.

VHS-Decode offers a more hardcore route. You still need a working VCR, but you tap the RF signal inside it, near the tape-head amplifier, before the machine turns it into ordinary video. This is not the antenna connector on the back. Suitable capture hardware records that earlier signal, and the computer reconstructs the picture, including time base correction, in software.

[... 3,323 words]

10:34 am / Vhs , Preservation , Codex , Automation , Ai assisted

Weftan, a backup plan that became a Rust rabbit hole

Weftan started as a fun backup plan. If TALA died hard, could I still have the layout engine around in some usable form?

I built Weftan with the assistance of Codex, using the Luna models available at the time. I supplied the ideas and directed the work; Codex did essentially all of the technical investigation and coding described here, including the reverse engineering and Rust implementation. AI also drafted this post.

TALA arranges diagrams for D2. Recovering its behavior and rebuilding it in Rust seemed like an interesting thing to try. That eventually became Weftan, and then a larger work-in-progress project called d2-rs.

Weftan was faster than the Go version of TALA when that was an external proprietary binary. The slightly annoying part is that TALA built into D2 was faster still. So naturally I started thinking about rewriting more of the surrounding thing too.

Recovering the behavior

Under my direction, Codex investigated TALA release artifacts: symbols, debugging information, disassembly, and comparisons with the executable. The Go code it reconstructed informed its Rust implementation. This was recovery work, and the provenance matters; I did not independently invent the same layout engine by looking at a few pictures.

Then TALA’s source became available in D2 v0.9.0. That changed the situation quite a bit. There was now an upstream source implementation to follow and compare against, instead of relying entirely on recovered behavior.

There is a lot hidden inside “put the boxes somewhere sensible.” Containers, labels, edge routes, and the order in which equally reasonable choices get considered can all affect the final result. Two diagrams can look basically the same while disagreeing on a lot of coordinates.

So the useful comparison was to give both implementations the same measured graph and seed, then compare the actual geometry. When something differed, there was a smaller question to investigate than “why is this picture weird?”

Matching a corpus

The recorded Weftan baseline reached exact matches on all 120 successful single-seed comparisons in the checked-in corpus. That included 2,964 boxes and 974 routes, along with their corresponding labels and node icons. One large-grid case hit the work limit in both engines and was recorded separately.

I was happy with that. I also want to keep the size of the claim attached to it: those are the cases in that corpus, with that reference version and comparison setup. It does not mean every possible D2 input will behave identically.

Being able to reproduce a mismatch is useful even before it is fixed. It gives the next round of work an actual target. Otherwise it is very easy to keep improving the appearance of one example without knowing whether another example got worse.

The comparison machinery ended up being a useful result of the experiment in its own right.

The plugin tax

Weftan plugs into D2 as an external layout process. D2 has to pass a graph across that boundary and get the result back. Built-in TALA does not pay that same process and protocol cost.

There are two different comparisons here. Against the old external proprietary Go TALA binary, Weftan was faster in my testing. Against TALA built into D2, the end-to-end measurements went the other way. The baseline changed, and that distinction matters.

Plugin overhead is a plausible explanation for at least some of the gap with built-in TALA, but I have not isolated it well enough to say it accounts for the entire difference. “Rust” on the box does not make the complete workflow faster by itself.

That leaves Weftan in a perfectly reasonable place as a fun experiment. The backup-plan motivation got me started, and I got a recovered engine, comparisons, and a much closer look at the behavior out of it.

It also left me curious about removing that boundary altogether.

Following upstream in Rust

The next rabbit hole is d2-rs: a pure-Rust implementation targeting native execution and WebAssembly, with the layout engines running in the same process. The TALA work from Weftan has moved into that effort.

I am interested in following upstream behavior and exploring parallel execution where it makes sense. Having the surrounding pipeline in Rust should also make it easier to investigate costs without the external plugin boundary sitting in the middle of every layout.

This is still under construction. There are working source-processing and rendering paths, but plenty of compatibility work remains. A working SVG example is a nice milestone; it does not establish that the renderer, all the diagram features, or the other engines are interchangeable with D2.

I also do not have a speedup to announce for the rewrite. That is something to measure as it develops.

For now, Weftan is public. It was a fun backup plan, it taught me things, and it apparently gave me another project to work on.

10:34 am / Rust , D2 , Reverse engineering , Wasm , Ai assisted , Codex

Keeping comma hardware repair answers somewhere findable

One of the things I keep working on is the unofficial comma hardware documentation. Somebody has a broken device, people help investigate, and eventually there is an answer somewhere in Discord.

I wrote the early documentation by hand. More recently, I’ve been using Codex to crawl through Discord discussions for updates, write additions, and straighten out existing material, under my direction. The repairs and reported results come from the community members credited in the docs. Codex also drafted this blog post.

Then somebody else has the same problem.

Discord is an annoying place for the answer to live. Finding the right conversation is work, the useful detail might be several messages away from the original question, and a person searching the web may never find it at all.

I hate walled gardens, and Discord is a bit of one. It is the same frustration that led me to build GitHub Wiki SEE back in 2021: people were putting useful information into GitHub wikis, but GitHub was blocking search engines from indexing it. I built a mirror to make those pages findable. Getting repair knowledge out of Discord and onto the public web comes from the same impulse.

So I keep trying to get those cases into a public document that search engines and AI assistants can read. Especially AI assistants, because people already ask them how to fix things. I want the actual repair knowledge to be available when somebody asks about their broken comma.

The useful part is the follow-up

The document is organized around cases: what the person sees, what might be wrong, and what has helped other people with that particular problem. It has grown quite a bit. I do not expect anybody to sit down and read the whole thing in order.

Recent additions and updates include more fuse repair reports, a fastboot-only recovery case, and another cable gotcha. These are the small details that can save somebody a lot of time if they are findable at the right moment.

Fuse cases are a good example of why the reports matter. A suspected fault and a confirmed repair are different pieces of information. Somebody coming back with “that fixed it” makes the original discussion much more useful. An unsuccessful repair is worth recording too, especially if the symptoms seemed to fit.

The repair work belongs to the community members doing it and reporting their results. My contribution here is collecting the cases, keeping the references, and updating the guidance as more information comes in.

Similar-looking problems need different answers

The fastboot-only case covers a specific recovery situation on the comma three and comma 3X. A device can still show up in fastboot while being unable to enter the EDL mode used by the current flasher. Seeing a USB device on the computer does not establish that the flasher can talk to it in the mode it needs.

That distinction needs to stay attached to the recovery notes. Otherwise a workaround for a particular failure starts looking like a general recommendation for everybody.

The comma four cable case is another good one. The included OBD-C cable does its intended installation job, but lacks the USB data lines needed for computer flashing. That is a pretty frustrating detail to discover while you are already trying to recover something.

Neither case needs a second set of instructions here that will drift out of date. The maintained document has the details and references.

Constant little updates

This work is mostly incremental. Another symptom, another result, a clearer explanation of which device a case applies to. Sometimes the useful update is just making it harder to confuse two similar situations.

I like having a place where those answers can accumulate. Whether somebody finds it through a search engine or asks an AI to help, I want them to end up at the relevant case, with the references and pictures available to check. The answer should have somewhere to point back to.

The docs are here. If you try a remedy from them, please report back whether it worked. That follow-up is part of what makes the next version of the document better.

I might need these docs myself soon. My own comma 3X did not boot when I tried powering it over USB. I have not figured out why yet, so I may get to find out how useful my own documentation is from the other side.

10:34 am / Comma ai , Openpilot , Hardware , Documentation , Ai assisted , Codex

Could the Crazy Taxi Arcade TAS use one fewer passenger?

I got curious about whether a Crazy Taxi tool-assisted speedrun could be slightly faster with one fewer passenger.

I investigated this Crazy Taxi route idea with the assistance of Codex, using the Luna models available at the time. I supplied the ideas and directed the work; Codex did essentially all of the technical investigation and coding, including the game analysis and viewer changes. AI also drafted this post.

I do not really have TAS skills. This was a “could that work?” rabbit hole that led to looking at passenger tables, destination selection, and the game clock. I have not made a faster run.

The tips are where the real TAS money is. Chaining Crazy Throughs and drifts can pile up money while carrying a single passenger. SJ’s run aims for the Crazy License, reaches $20,000 with four passengers, and uses the long Police Station trip for a huge combo chain. The run ends when the clock reaches zero. So the idea is to earn enough money from tips with fewer fares, then have less clock left to burn off.

The specific idea starts after the first drop-off at Cable Car Stop TOP, the plaza at the top of the hill. In SJ’s four-passenger route, the next pickup there goes to West Side Beach, followed by a passenger going to Popcorn Mania, and then one going to the Police Station.

[... 580 words]

10:34 am / Crazy taxi , Reverse engineering , Games , Tas , Ai assisted , Codex

Agent-assisted bilingual subtitles for group watches

I wanted English and Chinese subtitles on screen at the same time for a group watch. Some people read one language faster, some read the other faster. Switching tracks does not help everyone follow along together.

I built the subtitle tools with the assistance of Codex. I supplied the ideas and directed the work; Codex did the investigation and implementation, including the merging, timing alignment, and review reports. Codex also drafted this post.

Matching the tracks

The first script paired English and Chinese cues by their cue numbers, kept the English timestamps, and stacked the text. That worked for one pair of files, but it depended on both tracks numbering their cues the same way. Missing dialogue or extra signage cues can break that assumption.

Another pair needed more work. The timing difference between the tracks changed over the course of the movie, so shifting the whole Chinese track by a fixed number of seconds would not line it up.

I had Codex adapt the script to use the English track as the timing source:

  • Estimate the Chinese timing offset in local windows, then smooth those offsets.
  • Remap the Chinese cues onto the English timeline.
  • Merge Chinese text into the nearest overlapping English cue.
  • Keep Chinese-only cues as separate events.
  • Write a merged .srt and an alignment report.

Checking the result

The report listed low-confidence merges, Chinese-only cues, English cues without matching Chinese text, and the local offset anchors. That gave me timestamps to check instead of having to inspect the entire subtitle file.

I generated a track, skimmed the report, and spot-checked the questionable sections. When something looked wrong, I asked Codex to adjust the matching or improve the report, then generated it again.

Having Codex write and revise the scripts made this practical for a couple of group watches. I could ask for a change after trying the output with the actual files.

If you want to try it, give your coding agent both subtitle files and specify which track has the correct timing. Ask for the merged output and a report of uncertain matches and unmatched cues. Check those against the video before watching with everyone.

No repo link here. The subtitle files are not mine to redistribute.

It worked for our group watch. We had a good time, and everybody could follow along.

3:35 pm / Subtitles , Python , Agents , Localization , Tooling , Ai assisted , Codex

codex-status-json

I made a small Rust CLI that prints Codex status as JSON.

I built this tool with the assistance of Codex. I supplied the idea and directed the work; Codex did the technical investigation and implementation. Codex also drafted this post.

Update, October 11, 2026: this is now mostly a historical workaround for my Codex app workflow. The app now gives conversations a built-in way to read usage limits, so I no longer need this helper for that purpose. For manual checks, there is also the usage dashboard and the CLI’s /status command. That does not make every use of a standalone JSON tool obsolete, but it fills the gap that motivated mine. The rest of this post describes the workaround as it stood in June.

The short version is that I wanted Codex conversations to know my account, model, and quota state without opening the interactive TUI and reading /status like a person.

This is mostly useful with Codex goals. I am on the Pro 20x plan, which is the $200/month tier, so sometimes I really do have quota to burn. If I can read the remaining quota first, I can make a goal with an actual target instead of guessing how ambitious I should be.

The missing command

Codex has /status in the interactive CLI. It shows useful stuff: account, model, context, writable roots, and rate limits.

That is fine when I am sitting in the TUI. It is less fine when I want another Codex conversation to ask the same question.

There is an upstream issue asking for this exact kind of thing: a non-interactive JSON/headless replacement for /status. The issue is still open as I write this. There is also codex doctor --json, which is useful, but that is a diagnostic report. It is not the same as “tell me my account/model/quota status right now.”

So I made codex-status-json.

The first version was worse

The first version did what you would expect from a tool born out of impatience: it opened Codex in a PTY, sent /status, waited for the panel to settle, captured the terminal output, stripped ANSI escapes, and parsed the text.

It worked. It was also clearly a bad long-term place to live.

Terminal scraping is annoying because every little UI change can break you. The parser has to care about box drawing, timing, update notices, trust prompts, terminal width, and whatever else the TUI happens to render that day.

Still, it was useful. It gave me JSON like this:

{
  "schema_version": 1,
  "account": {
    "email": "user@example.com",
    "plan": "Pro",
    "raw": "user@example.com (Pro)"
  },
  "model": {
    "name": "gpt-5.5",
    "details": "reasoning high",
    "raw": "gpt-5.5 (reasoning high)"
  },
  "buckets": [
    {
      "name": "default",
      "limits": [
        {
          "kind": "5h",
          "remaining_percent": 99,
          "resets": "14:44",
          "reset_at_unix": 1780350290
        },
        {
          "kind": "weekly",
          "remaining_percent": 70,
          "resets": "08:23 on 7 Jun",
          "reset_at_unix": 1780845815
        }
      ]
    }
  ]
}

That was enough to unblock the thing I wanted.

The less cursed version

A few days later, I had Codex switch the default backend to Codex’s app-server.

That is a much better source than scraping the TUI. The tool starts:

codex app-server --listen stdio://

Then it sends JSON-RPC calls for:

  • account/read
  • account/rateLimits/read
  • config/read

That gives it the same kind of account, model, and quota information without pretending a terminal status panel is an API.

Important caveat: Codex app-server is documented, but it is also described as experimental and may change. So this is still not a supported codex status --json. It is just a more sensible workaround than screen scraping.

The PTY backend is still there as a fallback:

codex-status-json --backend pty

But the default is now:

codex-status-json --backend app-server

This is a bridge

I do not expect this to be the forever answer.

Codex’s usage and billing surface is still moving. OpenAI has already introduced things like banked rate-limit resets, so the status shape is not just “two counters and call it done.” There are plans, windows, reset times, secondary buckets, and whatever else comes next.

So I can see why OpenAI might not want to commit to a stable status JSON contract yet. That is fine. I still wanted something local that worked now.

This is the same kind of bridge as a lot of these small tools: use the available pieces, make the annoying gap tolerable, and be happy if the official thing eventually replaces it.

The repo

The project is here:

https://github.com/nelsonjchen/codex-status-command

Install it locally with:

cargo install --path .

The README has the options and limitations. The tests include a captured /status fixture so the text parser can at least fail loudly when Codex changes its display.

I would still rather have an official codex status --json. Until then, this is small, local, and useful enough.

3:03 pm / Codex , Rust , CLI , JSON , Automation , Ai assisted

Moving Gmail Send mail as to Cloudflare SMTP

I just finished moving my Gmail “Send mail as” setup to Cloudflare Email Service’s native SMTP submission. I can send email from my mindflakes.com address.

I built the SMTP relay described here with the assistance of Codex. I supplied the idea and directed the work; Codex did the technical investigation and implementation. Codex also drafted this post.

Update, October 12, 2026: Google is removing Gmail’s “Send as” support for third-party addresses in January 2027. Google’s announcement says existing setups continue during the transition, but new configurations may be restricted. Gmail-to-Gmail aliases and Google Workspace “Send as” are exempt. Incoming forwarding is unaffected. The sending setup described here now has an end date. Darn, I was looking forward to this for years. The rest of this post describes what worked in June.

Back in April, I had built a small Rust SMTP-to-Cloudflare REST API relay. Today I noticed Cloudflare had shipped native SMTP submission.

Part of the appeal here is minimizing vendors. Cloudflare already hosts the domain, handles incoming mail through Email Routing, and now can handle outgoing mail through Email Service too. For a personal domain, that can be cheap or free, and it is a nice alternative to paying for Google Workspace when a personal Gmail account is otherwise fine.

[... 722 words]

2:13 pm / Rust , SMTP , Cloudflare , Gmail , Email , Ai assisted , Codex

Plainize Clip

I made Plainize Clip, a tiny macOS app that cleans the current clipboard and quits.

I built Plainize Clip with the assistance of Codex. I supplied the idea and directed the work; Codex did the reverse engineering and implementation. Codex also drafted this post.

The short version is that I still wanted the old Plain Clip shape: copy weird text, launch the app, paste something boring.

Not a clipboard manager. Not a menu bar app. Not a background process sitting around in RAM forever. I launch it through Spotlight, it rewrites the pasteboard as plain text, and then it exits.

The annoying clipboard problem

Copying text on macOS is often not just copying text.

Sometimes it brings formatting. Sometimes it brings weird invisible characters. Sometimes it brings smart quotes, hard-wrapped lines, non-breaking spaces, tabs, and whatever else got dragged along from a PDF, web page, chat app, or ticketing system.

Usually I do not want a whole clipboard workflow. I just want the pasteboard to stop being annoying.

So Plainize Clip does the small version:

  1. Read the general pasteboard.
  2. If there is text, clean it.
  3. Write back plain text only.
  4. Quit.

That is the whole normal launch path.

Launch, clean, quit

The important behavior is that Plainize Clip is faceless.

[... 657 words]

11:05 pm / Macos , Swift , Clipboard , Appkit , Reverse engineering , Codex , Ai assisted

Blocking spammy caller names with VoIP.ms Call Hunting

I made a small Rust VoIP.ms CNAME blocker.

I built this caller-name blocker with the assistance of Codex, using GPT-5.4 with medium reasoning effort. I supplied the idea and directed the work; Codex did essentially all of the technical investigation and coding. AI also drafted this post.

The short version is that I wanted to block a caller by name instead of by phone number.

This sounds like it should be an option in VoIP.ms. For all the features VoIP.ms has, this is not one of them. So I made one for cheap.

The missing filter

VoIP.ms has a pretty capable CallerID Filtering feature. You can filter on specific caller ID numbers, phone book groups, anonymous callers, calls that do not match the North American number format, STIR/SHAKEN attestation level, and wildcard patterns like area-code-ish blocks.

That is all useful, but these asshole spam callers and scammers do not politely stick to one number. They rotate numbers. Blocking one just means the next call comes from another one. What I wanted to block on was the caller ID name.

VoIP.ms has CNAM support too, but as far as I can tell it is for displaying names, not filtering on them. Their blog post on stopping spam calls talks about filtering numbers, area codes, and anonymous callers, while describing CNAM as a way to identify callers before picking up. The Caller ID wiki page describes incoming Caller ID name lookup as an optional per-DID setting that can display a caller name for US and Canadian callers.

[... 709 words]

7:30 pm / Voip , Sip , Rust , Voip ms , Telephony , Ai assisted , Codex

Projects