<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archsetup/scripts, branch claude/hungry-franklin-09fffc</title>
<subtitle>Builds a full dev workstation from a bare Arch Linux install.
</subtitle>
<id>https://git.cjennings.net/archsetup/atom?h=claude%2Fhungry-franklin-09fffc</id>
<link rel='self' href='https://git.cjennings.net/archsetup/atom?h=claude%2Fhungry-franklin-09fffc'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/'/>
<updated>2026-10-06T07:41:47+00:00</updated>
<entry>
<title>docs(bluetooth): say what tlp resume does on the resume edge</title>
<updated>2026-10-06T07:41:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-10-06T07:41:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=d6c51ae3ae0cd51b3f3832e831e1d2c28f6b2276'/>
<id>urn:sha1:d6c51ae3ae0cd51b3f3832e831e1d2c28f6b2276</id>
<content type='text'>
The hook's header and its test docstring said the resume edge has no owner, which contradicted the later paragraph on tlp resume replaying the saved rfkill state. Both now say the replay puts bluetooth back where it was at suspend and applies no policy, so a block present at suspend stays.
</content>
</entry>
<entry>
<title>docs(bluetooth): drop the claim that zz- orders the hook after TLP</title>
<updated>2026-10-06T05:02:17+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-10-06T05:02:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=73a2a5e6a7fd582bccab858b3b7e9a1c61e6e750'/>
<id>urn:sha1:73a2a5e6a7fd582bccab858b3b7e9a1c61e6e750</id>
<content type='text'>
systemd-sleep runs every hook in parallel, so the prefix orders nothing. The comment now says where the settle still wins (a hibernate-class wake) and where the race with tlp resume's state replay stays open (a plain suspend), with the rfkill tell for it.
</content>
</entry>
<entry>
<title>feat(install): default DNS over TLS off on the Proton tunnel links</title>
<updated>2026-09-13T12:02:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-09-13T12:02:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=20196051d9cd25d775373a48fcf05dc258497301'/>
<id>urn:sha1:20196051d9cd25d775373a48fcf05dc258497301</id>
<content type='text'>
The resolved drop-in pins DNSOverTLS=yes for every link. Proton VPN (proton0) and the static Proton WireGuard profiles (wgpvpn) push an in-tunnel resolver (10.2.0.1) that answers plain port 53 and never completes TLS on 853, so every lookup through the tunnel hung. The Proton client recreates its NetworkManager profile on each connect, so a per-profile setting can't stick.

NM pushes a [connection-tunnel-dot] default matched on those two interface names to resolved on every activation. Wifi and everything else keep the strict setting. Ratio and velox already carry the drop-in by hand. This makes a rebuild carry it too.
</content>
</entry>
<entry>
<title>feat(bluetooth): restore the radio after a sleep cycle</title>
<updated>2026-08-21T14:25:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-21T14:25:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=d67c9fb1c397072beb3ca57e6bfd4ef54dcc266a'/>
<id>urn:sha1:d67c9fb1c397072beb3ca57e6bfd4ef54dcc266a</id>
<content type='text'>
Masking systemd-rfkill so TLP can own radios leaves the resume edge with no owner at all. TLP's sleep hook runs `tlp resume`, but its setting is DEVICES_TO_ENABLE_ON_STARTUP, and TLP has no ON_RESUME to pair with it. WiFi survives anyway because NetworkManager unblocks itself. Bluetooth has no equivalent, so it comes back soft-blocked and stays that way.

After a hibernate the controller is wedged as well, which is why unblocking alone is not the fix. I cleared rfkill by hand on velox this morning and scanning still returned nothing. Zero devices, in a room that gave seventeen a minute after the driver was reloaded. bluetoothd had logged "Failed to set mode" and "Failed to add device" at the instant of resume.

So the hook reloads btusb on a hibernate-class wake, then unblocks. That order matters: a freshly loaded btusb can come up blocked and would undo an earlier unblock. A plain suspend only unblocks, since it brings USB back intact and reloading there would tear down a working adapter for nothing.

It acts only where TLP's own config asks for bluetooth, so it re-asserts a declared intent instead of inventing one and a machine that deliberately keeps the radio off keeps it off. It exits zero on every path, because a failing sleep hook logs noise that outlives the cause.

The install sits next to the mask that creates the gap rather than beside the other installs, and creates /etc/systemd/system-sleep first. Arch does not ship it and install_executable is a plain cp, so without the mkdir a fresh machine would warn and end up with no hook.

Ten tests, and I checked they bite: dropping the TLP intent guard fails three, moving the unblock before the reload fails one.

This surfaced because hibernate was switched back on. The gap predates that and would have shown up the first time the laptop slept.
</content>
</entry>
<entry>
<title>fix(cmail): let Bridge setup finish without cmail-action</title>
<updated>2026-08-21T09:09:35+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-21T09:09:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=cfe9ab2c9a23757bf23d827c22d183e42f458541'/>
<id>urn:sha1:cfe9ab2c9a23757bf23d827c22d183e42f458541</id>
<content type='text'>
I moved the cmail-action install out of cmail-setup-finish.sh and left behind a guard that aborts when the tool is missing. The script mentions cmail-action five times and invokes it zero — every hit is a comment or that check. So a Bridge provisioning script hard-failed over a mail client it never touches, and a fresh machine got Bridge ready, the cert exportable, the service startable, and a script that refused to configure any of it.

It warns with the same remedy and carries on now. That also drops an ordering neither repo needs: Bridge setup no longer requires rulesets cloned and installed first.

The sequence around it hadn't caught up either. Both the checklist and the installer's completion message still opened with "clone claude-templates to ~/projects/claude-templates", the same dead path that broke the script, and neither mentioned rulesets at all. So the documented order was: clone a stale repo to a path that doesn't exist, log into Bridge, then run a script that aborts pointing at a directory nothing created. Both now run Bridge first and name rulesets as what it is — not a prerequisite for those steps, but the thing mbsync wants.

I also rephrased the header claim that the symlink "arrives on its own". True on a machine that runs agent sessions, since session start runs make install, and false on one that doesn't. It states the condition now instead of the consequence.
</content>
</entry>
<entry>
<title>fix(cmail): stop installing a tool that rulesets now owns</title>
<updated>2026-08-20T18:14:13+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T18:04:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=e312829a5b6175b7826088a55522afb89733b219'/>
<id>urn:sha1:e312829a5b6175b7826088a55522afb89733b219</id>
<content type='text'>
cmail-setup-finish.sh symlinked cmail-action from ~/projects/claude-templates, a path that stopped existing when claude-templates folded into rulesets as a subtree. The guard fired first and said "clone claude-templates first", which sent you looking for a repo that was present the whole time.

The step has no work left either way. cmail-action lives in rulesets at claude-templates/bin/, and rulesets' make install links that directory into ~/.local/bin at every session start, so it reaches each machine unattended. velox's symlink was written that way this morning without anyone asking.

So the script checks that the tool arrived instead of placing it, and names the install command if it hasn't. The Bridge state check, the cmailpass decryption and the cert handling all stay; those are still this script's job.

The header advertised the old behaviour too, so I rewrote it rather than leaving the prose describing a step that no longer exists.
</content>
</entry>
<entry>
<title>fix(post-rebuild-check): stop expecting a .claude directory every project names</title>
<updated>2026-08-20T16:45:58+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T16:45:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=368650016357f66a591a2e4c006f31b079ca23a5'/>
<id>urn:sha1:368650016357f66a591a2e4c006f31b079ca23a5</id>
<content type='text'>
Check 4 expects a tooling path wherever the project's own .gitignore names it, on the reasoning that the ignore file records what the project means to hold untracked. That holds for .ai and todo.org. It doesn't hold for .claude, because the bootstrap and the gitignore sweep write that line into every gitignore-mode project whether or not one ever exists there. The entry is aspirational, not a promise.

pearl, rsyncshot and yt-sync each name it and none has ever had one. Ratio is missing the identical directory in the identical three, and ratio was never rebuilt, so this is the steady state rather than drift. That is the same test CLAUDE.md is already excluded by, one comment above.

Dropping it costs no real signal. A project that genuinely carries a .claude, with rules and hooks from a language bundle, has it re-synced by sync-language-bundle.sh at every session start, so a true absence heals before this check would run.

velox now reports one finding instead of eight. Seven of the eight were this checker being wrong rather than the machine.
</content>
</entry>
<entry>
<title>feat(post-rebuild-check): let a machine declare which units it means to leave disabled</title>
<updated>2026-08-20T13:01:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T13:01:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=8731c966b8b3b184accae446aefbc13a073ec6e4'/>
<id>urn:sha1:8731c966b8b3b184accae446aefbc13a073ec6e4</id>
<content type='text'>
Check 2 treats "enabled" as a proxy for "will actually run", and the proxy is wrong for a unit nobody intends to enable on that box. velox has four: geoclue-agent is redundant because hyprland's exec-once starts the binary itself, emacs is started on demand by emacsclient, obs-record-watchdog only matters while recording, and obsbot-wb-guard needs an OBSBOT the laptop doesn't have. All four reported at every run.

Four permanent lines in front of every real finding teach me to skim the output, which is the argument check 4 already makes about CLAUDE.md. On velox this takes the run from 8 findings to 4.

The list is machine-local, read from $XDG_CONFIG_HOME/post-rebuild-check/units-expected-disabled. It can't live in the unit file, because obsbot-wb-guard is correctly enabled on ratio. One unit, a different right answer per machine.

An entry whose unit is enabled after all is reported too. The main loop can't catch that, since it skips any state that isn't disabled or linked, so the list gets its own pass. Without it a stale exemption sits there suppressing nothing, and the list becomes somewhere real findings go to die.

The exemption is tested last, so it never hides the dangling-symlink finding decided on the filesystem above it.
</content>
</entry>
<entry>
<title>fix(post-rebuild-check): stop overstating an unregistered signal account</title>
<updated>2026-08-20T12:44:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T12:44:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=c9e9edf1ac39fc9b3f9f08a1832d53a00fd684d4'/>
<id>urn:sha1:c9e9edf1ac39fc9b3f9f08a1832d53a00fd684d4</id>
<content type='text'>
The finding said agent-text relays into this machine, so a missing account broke paging for the whole fleet. That was true when the check was written and isn't now. agent-text walks AGENT_TEXT_RELAYS in order and skips itself, so a machine with no account pages through a relay that has one.

It's still worth flagging, because the fleet does lose paging when no relay host is registered either. I reworded the finding to say that instead.
</content>
</entry>
<entry>
<title>feat(post-rebuild-check): flag a stopped idle daemon and a read-only remote</title>
<updated>2026-08-20T07:17:09+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-20T07:17:09+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=3397463c0d40e8ec95fb6788a492ddde486076d9'/>
<id>urn:sha1:3397463c0d40e8ec95fb6788a492ddde486076d9</id>
<content type='text'>
Both are states where the machine looks finished and isn't, which is the whole point of this script.

Check 7 asks whether hypridle is running. Nothing else notices when it isn't. Idle lock and suspend stop happening, and the laptop runs until its battery is gone. That's how velox reset its RTC on 2026-08-19, which is what dropped it into check 6's clock and DNS deadlock. The check asks whether the daemon is alive rather than why it might not be, so a crash and a stale caffeine surface alike. It's gated on hypridle being installed, since only Hyprland machines get it.

Check 8 asks whether the working repos can push. The installer clones them from the read-only https endpoint. That's right for someone installing archsetup with no key on my server, and wrong for my own machines. Nothing about the tree shows it. velox's dotfiles remote sat that way for four days and announced itself as a 403.

Only my own read-only endpoint is flagged. An https remote elsewhere may push fine through a credential helper, and guessing about hosts this machine doesn't own would stand noise in front of real findings.
</content>
</entry>
</feed>
