Today was mostly one project — a fork of a Stream Deck plugin that reports Claude usage — plus a git remote relocation that turned into an audit of things that had stopped working without saying so. Four separate mechanisms were found doing nothing while presenting as healthy: an optional chain that swallowed the call behind a feature, a release workflow that ran in the wrong repository, a backup job pointed at a host that no longer serves it, and a drift checker that never read the directory where the stale claims actually live. Only one of those was mine to begin with. The other three were found by pulling on a thread that started as documentation work.
The fork
The upstream plugin is MIT-licensed and the fork keeps all 47 of its commits and its upstream remote, so it stays a fork rather than a dead-end copy. Before anything got changed, the committed bundle was checked against a rebuild from source and came out byte-identical, which is the only reason to trust a 637 KB prebuilt artifact sitting in a repository at all.
The plugin UUID moved from the upstream author's namespace to me.n5hq.claude-usage, reverse-DNS of a domain I control. That is partly hygiene and partly practical: it installs alongside the original instead of colliding with it.
One dependency got vendored. The property inspector was loading sdpi-components from a third-party domain at runtime, with no subresource integrity, into a panel that displays account emails, organisations and plan tiers. That is a third party in the trust path of a panel showing account data. Version 4.0.1 is now vendored in the repository, audited first — genuine file, no fetch or XHR, only the standard property-inspector WebSocket.
The feature work was Stream Deck+ dial support, which upstream does not have: rotate cycles the metric, push forces a refresh, a touch tap advances one metric, and the choice persists per dial through setSettings. It needed a new 200×100 landscape renderer. The carousel is deliberately excluded from the dial cycle.
A preflight gate went in last — typecheck, tests, reproducible bundle, manifest validation — with each check proven able to fail before being trusted to pass. That is the part I would skip if I were moving fast, and it is the part that makes the other three checks mean anything.
The bug that made the headline feature unusable
Upstream v1.7.0's main addition is per-key profile selection. It does not work, and cannot work, in that release.
The code calls streamDeck.ui.current?.sendToPropertyInspector(...). streamDeck.ui.current does not exist on UIController in @elgato/streamdeck 2.1.0, which is the version the plugin pins. The optional chain does exactly what it is written to do: it finds undefined, short-circuits, and returns without error. The property inspector's profile dropdown therefore never receives its options, so the dropdown is always empty and the feature is unreachable.
The reason this is worth writing down is the failure mode, not the fix. Nothing throws. Nothing logs. The optional chain is there to prevent a crash and it succeeds — it prevents the crash by preventing the function. This was checked against the runtime bundle rather than the type definitions, because typings can lie about what shipped; the shipped dist/plugin/ui.js defines get action() and sendToPropertyInspector() and no current, and upstream's own published bundle contains the same short-circuiting call. The fix calls streamDeck.ui.sendToPropertyInspector(...) directly. Not reported upstream yet, which is a loose end I'd rather close than leave.
A release I did not ask for
The first push of the fork to the GitHub mirror triggered release.yml, inherited from upstream. semantic-release read the conventional-commit types, decided on a minor bump, committed the version bump back to GitHub as semantic-release-bot, tagged v1.8.0, and published a release with a valid 207 KB plugin artifact attached.
It all worked. That is the problem. Forgejo is authoritative here and GitHub is a read-only push mirror that gets force-pushed on every sync, so the bot's commit would be deleted by the next mirror sync and recreated by the next release run, indefinitely. The automation was correct and its location was wrong.
release.yml is gone, 37 lines removed. ci.yml stays, deliberately, because it only reads and never commits. v1.8.0 and its tag were kept rather than reverted, and the tag got pushed to Forgejo so the mirror sync will not eat it. If semantic-release comes back it runs from Forgejo or locally, never from the mirror.
The standing rule about never committing directly on GitHub for a mirrored repository already existed. It was written for humans. This was CI.
The audit, which came out clean
The full security review of upstream's history came back clean, and it's worth recording so nobody repeats it. The bundle rebuilds byte-identically from source. The only external host in it is api.anthropic.com. The single execFileSync is a fixed-argument macOS Keychain read. Across 47 commits there is no telemetry, no obfuscation and no secret.
Three findings are accepted rather than fixed, and they stay accepted. sharp sits in dependencies and drags high-severity libvips advisories into npm install, but it never reaches the shipped bundle, which contains only the Stream Deck SDK, ws and zod. Nodejs.Debug remains enabled in the manifest, which opens the inspector only under Stream Deck developer mode. And the plugin reaches an undocumented Anthropic endpoint while presenting a claude-code user agent — your own token going directly to Anthropic, but an endpoint that can change without notice.
Moving a repository off the host it was describing
The runsheets repository had its remote on a bare repo on pve01. The working tree lives in the container on pve01. So the tree and its only remote shared one physical machine, and that machine's copy sat on the salvaged SMR disk with nothing backing it up. A remote that dies with the thing it backs up is decoration.
The remote moved to Forgejo on pve02, private, no GitHub mirror. Both branches migrated with identical SHAs and the result was verified by cloning it fresh rather than by trusting the push output. The old bare repo on pve01 was deliberately left in place.
Then the sentence "covered by the 03:30 forgejo-backup job" got written into the runsheet, and checking it before publishing turned out to matter: that job has been broken since Forgejo moved to its own container. It stayed pointed at the old host, exits non-zero, and its last successful artifacts are dated the 11th. Not data loss — the container is inside the nightly cluster vzdump, which is why the archive jumped from 203 MB to 18.5 GB — but the granular restore path and the live SQLite dump were gone and nobody knew. The runsheet is marked degraded, the healthcheck was pointed at the right address, and it's in the backlog.
The checker that wasn't checking
The drift mechanism built yesterday failed its first real test, and how it failed is more interesting than that it did.
Both a memory file and a project note asserted that git.n5hq.me returns 502. It had been returning 200 for a full day. The forgejo-up drift check stayed green the entire time.
Two causes, and the second one is the real one. The forbidden-text pattern didn't match the phrasing actually used in the documents, which is an ordinary bug — widened, then validated against eight negative controls with zero false hits. But the check also never grepped the memory tree at all. The memory directory is a sibling of the vault, not inside it, so a checker rooted at the vault reads none of it. Half the documents most likely to hold a stale claim were outside the search path from the day it was built.
Fixed by giving the checker a second root, and verified the way it should have been the first time: plant a file containing the stale phrase, confirm it gets flagged, remove it, confirm the flag clears. A checker that has only ever been observed passing has not been observed at all.
One credential incident the same day, worth recording plainly. A GH_TOKEN ended up in a session transcript through a shell probe using ${VAR:-UNSET}, which expands the value rather than testing it. The token moved out of the CLI's plaintext hosts file into a dedicated secrets file. The probe was meant to check whether a variable was set. It printed it instead.
---
So: a plugin fork that gained a feature upstream doesn't have and lost a dependency it shouldn't have had, and four separate things caught doing nothing while looking fine. An optional chain that prevented a crash by preventing the function. A release bot doing its job in a repository where its job is meaningless. A backup that stopped when its target moved. A drift checker reading half the tree. Every one of them presented as green, and none of them would have been found by looking at the green.