I moved my repos from pre-commit to prek, a reimplementation of the same framework in Rust. It reads your existing .pre-commit-config.yaml unchanged, so the switch is mostly a no-op. Two things sold me: it installs as a single binary with no Python of its own (uv tool install prek, or a standalone script), and for language: python hooks it builds the hook environments with uv instead of pip and virtualenv, which is a lot faster on a cold cache.
“Drop-in” came with one wrinkle. prek’s YAML parser is stricter than pre-commit’s, and it rejected two hook manifests that pre-commit had been quietly accepting for years. Both turned out to be real bugs in the upstream hooks.
The first was a duplicate key. The gruntwork-io/pre-commit version I was pinned to, v0.1.20, shipped a golangci-lint hook that declared language twice:
pre-commit parses with PyYAML, which silently keeps the last of two duplicate keys and moves on. prek’s Rust parser treats a duplicate mapping key as an error and refuses to run. It’s fixed upstream in v0.1.30 (gruntwork PR #134), so bumping the hook version cleared it.
The second was deprecated stage names. The pre-commit-hooks version I was pinned to, 4.4.0, still used the old git-stage spelling:
stages:[commit,push]# deprecatedstages:[pre-commit,pre-push]# current
pre-commit renamed the commit stage to pre-commit and push to pre-push a while back and started warning on the old names; newer tooling rejects them outright. pre-commit-hooks uses the modern names in 6.0.0, so upgrading to it sorted it.
Neither was prek’s fault. It just stopped tolerating manifests that were already wrong.
Once everyone on the repo is on prek, you might not need the pre-commit-hooks repo at all. prek ships Rust-native versions of the common checks (trailing whitespace, end-of-file fixer, YAML and JSON validation) that you pull in with repo: builtin instead of cloning and pinning the upstream hooks. Just note that repo: builtin only works under prek, so it’s for repos where you’ve moved the whole team over and don’t need the config to keep running under stock pre-commit.
The reason I bother with any of this: I run the exact same hooks locally and in CI, through j178/prek-action. If a formatter or a lint rule is going to fail, I want it fixed in the commit, not after a five-minute CI round-trip and a “fix lint” follow-up commit. Enforcing it across a whole team kills a lot of that churn.
The catch is that hooks have to stay fast, because they run on every commit. Formatters, linters, a YAML syntax check: fine. The full test suite or Playwright browser tests: no. Those go in CI, where slow is acceptable and the same prek config runs --all-files anyway.
I run OpenWrt as the firmware on my core switch, a Zyxel XGS1210-12: eight gigabit ports, two 2.5GbE copper ports, and two SFP+ cages that do 10G, for around $120. If you want a cheap, multi-gig managed switch that runs open-source firmware and still does VLANs and the rest, I think it’s the best option right now. It’s on the hardware list in the OpenWrt Beginner’s Guide, which is where I’d check for current recommendations since this moves quickly.
For me the appeal is owning the hardware outright, more than any feature list. With open firmware I can see exactly what the switch runs, test a fix myself when I hit a bug, and send a patch upstream if it comes to that. Closed gear leaves you one update away from changes that suit the vendor more than you, and for something as tied to privacy and security as networking equipment, the transparency is worth it.
UniFi is the obvious alternative, and credit where it’s due: its UI and single-pane-of-glass view of a whole network are genuinely nicer, so going the OpenWrt route is hard mode. But it costs a lot more. A comparable UniFi switch with 2.5G and a 10G SFP+ uplink, the Pro Max 16, runs a few hundred dollars ($399 with PoE, less without) versus $120 here, and that adds up fast across several switches. The open-source enterprise options don’t help on price either: SONiC, DENT, and Cumulus all need $1,000-plus whitebox gear.
Under the hood it’s a Realtek RTL9302, and getting the 2.5G copper ports working was genuinely hard. The PHY spoke a proprietary Realtek protocol the Linux kernel couldn’t drive, and the fix took getting proper support upstream so it could switch modes by link speed. The whole saga is in issue #19640; the breakthrough landed in PR #19843 in August 2025, and the rev B1 board got its fix in PR #21605 in January 2026. That work came from Birger Koblitz (who started OpenWrt’s Realtek port), Tobias Schramm, Markus Stockhausen, Jan Hoffmann, Jonas Jelonek, and Sander van Heule. I’m surely forgetting people, so check the linked PRs and issue thread for the full list.
None of it came with a datasheet. Realtek doesn’t publish one, so the whole thing was reverse-engineered from GPL source dumps and a lot of register poking. Sander van Heule documents the internals at svanheule.net and even built a web tool for browsing these chips’ registers. People doing that for free, with no documentation, is the reason a $120 switch can run mainline Linux at all. I appreciate it a lot.
OpenWrt itself is worth appreciating too. It’s a remarkably flexible Linux build, and with all sorts of tricks to squeeze it down, it’ll run a full, configurable network OS on devices with only a few megabytes of flash. Getting real Linux into that little space is the cool part.
A couple of things to flag. There’s no PoE on this model, so APs and cameras need injectors or a separate PoE switch. And the 2.5G work is on the snapshot builds rather than the stable release for now, so flash a recent snapshot that matches your board revision (mine’s the B1). As of writing that’s r35109-f5d928e52a. Beyond that it’s been solid for me, and for a cheap, open, multi-gig managed switch it’s the one I’d get.
I run Claude Code, and its VS Code extension, inside dev containers. The reason is isolation. Editor extensions and the toolchains a project pulls in have been a real supply-chain vector lately, and a dev container keeps a compromised one off my host: the extension, the agent, and everything npm or uv installs all live inside the container, not on my machine.
That isolation changes how I think about permissions. Because the blast radius is the container and not my laptop, I’m comfortable letting the agent do far more inside it than I would on bare metal.
The container is a small file. The Claude Code dev container feature installs the CLI and the extension into the container, and I run as a non-root user:
With that boundary in place I work on trimming the unnecessary prompts, not stopping them entirely. I commit a wide allowlist to .claude/settings.json and turn on acceptEdits so routine file writes don’t prompt either:
{"permissions":{"defaultMode":"acceptEdits","allow":["Bash(make test:*)","Bash(make lint:*)","Bash(uv run pytest:*)","Bash(npm run build:*)","Bash(git add:*)","Bash(git commit:*)"],"deny":["Read(.env)","Read(**/.env)"]}}
It goes in .claude/settings.json, not .claude/settings.local.json, on purpose. When you click “yes, don’t ask again”, Claude Code writes the rule to the local file, and that file gets wiped every time the container rebuilds. Commit the allowlist and it survives rebuilds and travels with the repo.
The container isn’t a free-for-all, so I keep two guardrails inside it. It still holds my code and a token, so I don’t blanket-allow uv run or npm install: uv run runs whatever follows it, and npm install <pkg> runs package install scripts, which is how a lot of npm supply-chain attacks land. I pin the inner command (uv run pytest) and leave the runners prompting. I also keep the container’s GitHub token read-only and push from the host, so a bad suggestion can’t push or open a PR on its own.
If you don’t want a full dev container, Claude Code added a built-in Bash sandbox recently. Run /sandbox and it isolates shell commands at the OS level, Seatbelt on macOS and bubblewrap on Linux, restricting what each command can read, write, and reach on the network. In auto-allow mode it runs sandboxed commands without a prompt while git push and friends still ask. It’s a lighter boundary than a container, and you can run it inside one as a second layer (set enableWeakerNestedSandbox when nested, since the container is already the outer boundary).
None of this is a hard sandbox on its own. Permission rules are enforced by Claude Code, not the OS, and the sandbox proxy doesn’t inspect TLS, so a broad domain allowlist can still leak. I’ve always been more worried about approving the one thing I shouldn’t than about the prompts themselves, and I’m still working out how to shrink the blast radius further. This is early, and I expect parts of it to look risky in hindsight.
Macvlan is a Docker networking mode that gives each container its own MAC and IP on the network the host port is plugged into. The container shows up on the subnet as its own device, gets a DHCP lease from the router, and talks to the LAN directly instead of going through the Docker bridge.
I have a box with multiple Ethernet ports, each port on a different VLAN. Some containers sit on the port assigned to a guest-style VLAN that can only reach the internet, not other hosts on the LAN. That worked fine in the default bridge macvlan mode while each port only had one service on it. Once I added more services to the same port, I hit a problem I hadn’t thought about: containers sharing a macvlan parent in bridge mode can freely ARP and talk to each other inside the kernel, which defeats the VLAN-level isolation.
Macvlan has a private mode for exactly this. The kernel drops frames going between siblings on the same parent. Outbound traffic via the gateway still works. No switch cooperation, no extra VLANs.
One caveat: the host itself can’t reach its own containers over that interface. That’s true for any macvlan setup, not specific to private mode. For my case it’s fine since the containers just need internet access and to be reachable from other subnets through the router.
Distributing one-off Python scripts has gotten a lot better recently. PEP 723 added a standard way to declare dependencies and a minimum Python version inside a single .py file, and uv makes running them painless. I used this to share a script with the team and it just worked across everyone’s machines.
Before this, you’d end up writing some wrapper shell script that creates a virtualenv, makes sure the right Python version is being used, installs the dependencies, and tries to keep everything in sync. Brew-installed Python was especially bad for this because brew upgrade can silently swap your Python version and break every virtualenv pointing at it. Even version managers like asdf or pyenv don’t really help because the recipient still needs the right version installed through their manager. A lot of ceremony for a single file.
The # /// script block is TOML. You don’t have to write it by hand either, uv add --script example.py 'requests' will add it for you.
The recipient just needs uv installed:
uv run check_status.py
uv reads the inline metadata and installs the dependencies in an isolated environment. If the required Python version isn’t on their machine, uv downloads it automatically. No virtualenv, no version manager, no wrapper script.
You can also lock dependencies with uv lock --script example.py for reproducible runs. For bigger tools that span multiple files, uv tool install is the next step up and can install from a private PyPI index.
I finally got to a local LLM setup that feels pretty usable within a 16 GB VRAM constraint (around 40 tok/s).
As far as I can tell, Open WebUI is still the best open source chat interface for local models. I am running it on a Ryzen 5 5600 box with an RTX 5060 Ti 16 GB card, with Qwen3.5-9B GGUF at Q8_0 as the main local model. I started with Ollama because it is the popular default. For Qwen 3.5 though, I hit a few open issues at the time that made llama.cpp easier for my setup: much slower inference than llama.cpp with the same model, long stalls on later turns in a conversation, and broken /no_think handling for Qwen. It is a very new model, and I am sure the Ollama project will get those fixed.
llama-server exposes an OpenAI-compatible API, so Open WebUI mostly treated it like a config swap.
The more interesting part is that Qwen 3.5’s newer hybrid architecture seems to help a lot here. The 9B model feels much better than I would have expected for the size, and this is the first local setup I have had on this machine that felt worth keeping around.
From what I had been reading, thinking mode also did not seem worth the wait for a setup like this, and that matched what I was seeing. Qwen 3.5 defaults to thinking, but for my use it was usually slower, often added 30-60 seconds, and sometimes got stuck in long thinking loops without noticeably improving the answer. What finally worked reliably here was a newer llama.cpp build with LLAMA_ARG_JINJA=1 and LLAMA_ARG_THINK_BUDGET=0.
Most of the other tuning was about using the remaining VRAM well once the 9B model itself had already taken roughly 9 GB. LLAMA_ARG_N_PARALLEL=1, LLAMA_ARG_FLASH_ATTN=1, q8_0 KV cache, LLAMA_ARG_BATCH_SIZE=4096, and LLAMA_ARG_UBATCH_SIZE=1024 were what got the whole setup to around 14 GB and let me spend the rest on context instead of spilling into system memory. The next size up looked much more likely to spill into RAM. I wanted to see what I could get done with this Nvidia card first, since it still seemed like the safest compatibility bet.
For serious work I would still use frontier hosted models. The local setup is useful enough to keep around for narrower cases, especially redacting or cleaning text before sending it to a cloud model.
Full example gist with the commented minimal two-container compose file:
I recently got a small fix merged into metrics-server, which powers kubectl top and is used for horizontal pod autoscaling in Kubernetes. It’s not exactly a core component, but most production clusters have it installed.
I’ve been using ArgoCD at work lately for deploying Helm charts through a GitOps flow, and I noticed that the metrics-server APIService resource kept showing as “OutOfSync” even though nothing had actually changed.
The issue was that the Helm template always rendered the insecureSkipTLSVerify field explicitly, but Kubernetes omits it from live resources when it’s false (the API default). This caused ArgoCD to see a constant diff.
The fix was to conditionally render the field using {{- with .Values.apiService.insecureSkipTLSVerify }} so it only appears when set to true. Same approach other projects like KEDA have used.
It’s a tiny fix, but it’s satisfying to have a change merged into something as widely deployed as metrics-server.
Earlier this year, I wanted to help seed Linux distribution torrents using cheap VPS servers that offer terabytes of monthly bandwidth for less than $20/year. To maximize cost efficiency, I needed something as memory-efficient as possible.
After trying qBittorrent, rTorrent, and Transmission (via Docker) on a 1GB RAM VPS, I kept running into OOM issues and configuration headaches. Those clients are great, but running them in 1GB RAM doesn’t seem to be a design goal. It’s probably still possible with enough tweaking.
I ended up building distro-seed, a lightweight Go-based BitTorrent seeder using anacrolix/torrent. It’s a Go library that handles all the BitTorrent protocol details while letting you build exactly what you need without the overhead of a full client.
So far, it’s seeded 1.25 TB of Linux distributions.
The project includes an Ansible playbook to set up a fresh Ubuntu VPS for automatic seeding. The whole setup is simple: configure your torrent sources in a YAML file, run the playbook, and let it seed.
Today, I came across a bug where a Celery worker wasn’t gracefully shutting down, and it was causing some odd “Connection Refused” errors from requests within the task being run by the worker. It was also shutting down before it could send errors to Rollbar/Sentry for the team to know they need to address it.
This was happening because the entrypoint had a script that effectively did this:
echo "starting celery worker"
celery -A tasks worker