<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>brtkwr.com</title><link>https://brtkwr.com/</link><description>Recent content on brtkwr.com</description><generator>Hugo</generator><language>en</language><lastBuildDate>Thu, 09 Apr 2026 21:00:00 +0100</lastBuildDate><atom:link href="https://brtkwr.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Making hot sauce is more engineering than cooking</title><link>https://brtkwr.com/posts/2026-04-09-making-hot-sauce-is-more-engineering-than-cooking/</link><pubDate>Thu, 09 Apr 2026 21:00:00 +0100</pubDate><guid>https://brtkwr.com/posts/2026-04-09-making-hot-sauce-is-more-engineering-than-cooking/</guid><description>&lt;p>&lt;strong>TL;DR&lt;/strong> — I did a hot sauce making course at &lt;a href="https://eastonchilli.com/" class="external-link" target="_blank" rel="noopener">Easton Chilli&lt;/a> in Bristol. Came home with eight bottles and a new appreciation for the art and science behind it.&lt;/p>
&lt;h2 id="why-i-signed-up">
 Why I signed up
 &lt;a class="heading-link" href="#why-i-signed-up">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I like hot sauce. I go through bottles faster than I&amp;rsquo;d care to admit, but making my own had never crossed my mind. My partner got me an &lt;a href="https://eastonchilli.com/" class="external-link" target="_blank" rel="noopener">Easton Chilli&lt;/a> course for Christmas. It got rescheduled once, then I sat on it for months before booking. Four months later, I finally went.&lt;/p></description></item><item><title>Switching OpenClaw to CLI backends for Claude and Codex</title><link>https://brtkwr.com/posts/2026-04-09-switching-openclaw-to-cli-backends-for-claude-and-codex/</link><pubDate>Thu, 09 Apr 2026 09:00:00 +0100</pubDate><guid>https://brtkwr.com/posts/2026-04-09-switching-openclaw-to-cli-backends-for-claude-and-codex/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> OpenClaw can call Claude and Codex through their CLI tools instead of hitting APIs directly. CLI backends give you better session handling and remove API key management. Switching over requires cleaning up some wizard-generated config. Extra usage billing still applies for Claude.&lt;/p>
&lt;h2 id="why-cli-backends">
 Why CLI backends
 &lt;a class="heading-link" href="#why-cli-backends">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>OpenClaw supports two ways of talking to model providers: the &lt;strong>embedded API&lt;/strong> (sends HTTP requests using an API key or token) and &lt;strong>CLI backends&lt;/strong> (shells out to the provider&amp;rsquo;s CLI tool, e.g. &lt;code>claude&lt;/code> or &lt;code>codex&lt;/code>).&lt;/p></description></item><item><title>Migrating OpenClaw from Jetson Nano to a VPS</title><link>https://brtkwr.com/posts/2026-04-08-migrating-openclaw-from-jetson-nano-to-a-vps/</link><pubDate>Wed, 08 Apr 2026 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-04-08-migrating-openclaw-from-jetson-nano-to-a-vps/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> I moved my OpenClaw setup from a Jetson Nano to a VPS. Persistent block volume for state, Node 24 LTS instead of a hand-compiled Node 22, and the whole migration took about 20 minutes once I stopped fighting ARM constraints.&lt;/p>
&lt;h2 id="why-leave-the-jetson">
 Why leave the Jetson
 &lt;a class="heading-link" href="#why-leave-the-jetson">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I&amp;rsquo;ve been running OpenClaw on a Jetson Nano since February. It worked, but the maintenance cost kept climbing. The Nano runs Ubuntu 18.04 with an ancient glibc, so I had to &lt;a href="./posts/2026-03-02-upgrading-openclaw-to-latest-node22-on-jetson-nano/" >compile Node 22 from source&lt;/a> (27 hours). Bun global installs broke when OpenClaw tightened plugin manifest validation. Upgrades were a gamble.&lt;/p></description></item><item><title>Fixing the post-quantum SSH warning</title><link>https://brtkwr.com/posts/2026-04-08-post-quantum-ssh-with-ml-kem/</link><pubDate>Wed, 08 Apr 2026 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-04-08-post-quantum-ssh-with-ml-kem/</guid><description>&lt;p>OpenSSH 10.2 warns you when a connection isn&amp;rsquo;t using post-quantum key exchange. On the client side (macOS), it works out of the box. On the server side, you might need to upgrade OpenSSH, which on Ubuntu 18.04 means building from source.&lt;/p>
&lt;h2 id="the-warning">
 The warning
 &lt;a class="heading-link" href="#the-warning">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I SSH&amp;rsquo;d into my Linux box and got this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">** WARNING: connection is not using a post-quantum key exchange algorithm.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">** This session may be vulnerable to &amp;#34;store now, decrypt later&amp;#34; attacks.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">** The server may need to be upgraded. See https://openssh.com/pq.html
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The warning comes from the client. OpenSSH 10.2 shows it when the negotiated key exchange algorithm is classical, which usually means the server doesn&amp;rsquo;t support post-quantum algorithms.&lt;/p></description></item><item><title>GCS uniform bucket access silently breaks project owner reads</title><link>https://brtkwr.com/posts/2026-04-02-gcs-uniform-bucket-access-breaks-project-owner-reads/</link><pubDate>Thu, 02 Apr 2026 13:00:00 +0100</pubDate><guid>https://brtkwr.com/posts/2026-04-02-gcs-uniform-bucket-access-breaks-project-owner-reads/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Enabling &lt;code>uniform_bucket_level_access&lt;/code> on a GCS bucket disables the legacy role bindings that project owners rely on for object access. Your Terraform SA with &lt;code>roles/owner&lt;/code> will get 403s reading objects it could read moments earlier. Add explicit &lt;code>roles/storage.objectViewer&lt;/code> bindings on the bucket &lt;em>before&lt;/em> flipping the setting.&lt;/p>
&lt;hr>
&lt;p>I&amp;rsquo;d recommend uniform bucket-level access for most buckets. Without it, GCS uses two overlapping permission systems: bucket-level IAM and per-object ACLs. You can&amp;rsquo;t see object ACLs in &lt;code>get-iam-policy&lt;/code>, so auditing access means checking two places. With uniform access, you manage all permissions through bucket-level IAM. Google recommends it for new buckets, and you can enforce it project-wide via org policies.&lt;/p></description></item><item><title>What we can all learn from the Claude Code source</title><link>https://brtkwr.com/posts/2026-04-01-what-we-can-all-learn-from-the-claude-code-source/</link><pubDate>Wed, 01 Apr 2026 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-04-01-what-we-can-all-learn-from-the-claude-code-source/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> Anthropic accidentally shipped the Claude Code source map; I went through it looking for patterns worth borrowing. The codebase reads like accumulated postmortem residue — the compaction circuit breaker exists because telemetry showed sessions burning 250K tokens on doomed retries, the LRU cache was rewritten after it leaked 300 MB, the keychain code references a specific CrowdStrike incident. Nothing was designed from scratch. The findings that weren&amp;rsquo;t widely covered:&lt;/p></description></item><item><title>Migrating from kube-prometheus-stack to Google Managed Prometheus</title><link>https://brtkwr.com/posts/2026-03-27-migrating-from-kube-prometheus-stack-to-google-managed-prometheus/</link><pubDate>Fri, 27 Mar 2026 06:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-27-migrating-from-kube-prometheus-stack-to-google-managed-prometheus/</guid><description>&lt;p>I spent a couple of days migrating our monitoring stack from self-hosted kube-prometheus-stack (KPS) to GKE&amp;rsquo;s native Google Managed Prometheus (GMP). The end result is simpler, cheaper, and removes about 2 TiB of persistent storage we no longer need. But the migration had enough non-obvious gotchas that I wanted to write it all down.&lt;/p>
&lt;h2 id="why-migrate">
 Why migrate?
 &lt;a class="heading-link" href="#why-migrate">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>kube-prometheus-stack bundles Prometheus, Alertmanager, Grafana, node-exporter, kube-state-metrics, and the Prometheus operator into one Helm chart. It works well, but on GKE you&amp;rsquo;re duplicating what the platform already provides:&lt;/p></description></item><item><title>Why VPA ignores single-replica pods</title><link>https://brtkwr.com/posts/2026-03-22-why-vpa-ignores-single-replica-pods/</link><pubDate>Sun, 22 Mar 2026 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-22-why-vpa-ignores-single-replica-pods/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: VPA&amp;rsquo;s updater defaults to requiring 2 replicas before it will evict a pod. Single-replica deployments are silently excluded from auto-healing — even if they&amp;rsquo;re crashlooping. You can override this with &lt;code>minReplicas: 1&lt;/code> in the VPA spec, no cluster upgrade needed.&lt;/p>
&lt;hr>
&lt;p>I had a pod stuck in CrashLoopBackOff for 20 hours with 192 restarts. &lt;a href="./posts/2026-02-01-getting-vpa-resourcepolicy-right-in-helm-charts/" >VPA&lt;/a> had pushed its memory limit down to ~157Mi — too low for a Python service — and the pod was &lt;a href="./posts/2026-02-24-go-containers-and-the-oom-killer/" >OOMing&lt;/a> on every start.&lt;/p></description></item><item><title>Getting my Apple Watch workout history into Garmin</title><link>https://brtkwr.com/posts/2026-03-21-converting-apple-health-workouts-to-garmin/</link><pubDate>Sat, 21 Mar 2026 15:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-21-converting-apple-health-workouts-to-garmin/</guid><description>&lt;p>&lt;strong>TL;DR&lt;/strong> — I switched from an Apple Watch to a Garmin and wanted to bring my workout history with me. Apple&amp;rsquo;s data export turned out to be surprisingly lossy — heart rate gets aggregated into 15-minute chunks. I ended up building an iOS app to read HealthKit directly, a Python converter to produce FIT files, and an upload script to push everything to Garmin Connect. 255 workouts, full-resolution heart rate, running dynamics, GPS — all transferred.&lt;/p></description></item><item><title>Fixing Spotlight Search for Applications on macOS</title><link>https://brtkwr.com/posts/2026-03-17-fixing-spotlight-search-for-applications-on-macos/</link><pubDate>Tue, 17 Mar 2026 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-17-fixing-spotlight-search-for-applications-on-macos/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> If Spotlight can&amp;rsquo;t find your apps, rebuild the Launch Services database with &lt;code>lsregister&lt;/code>. It takes a few seconds and doesn&amp;rsquo;t require a reindex of your entire drive.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Spotlight stopped finding some of my applications. Searching for apps I knew were installed returned nothing, or surfaced web results instead. Finder search was equally useless.&lt;/p>
&lt;h2 id="what-didnt-work">
 What didn&amp;rsquo;t work
 &lt;a class="heading-link" href="#what-didnt-work">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>My first instinct was to reindex Spotlight for &lt;code>/Applications&lt;/code>:&lt;/p></description></item><item><title>jj for Git Users: A Practical Walkthrough</title><link>https://brtkwr.com/posts/2026-03-08-jj-for-git-users/</link><pubDate>Sun, 08 Mar 2026 22:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-08-jj-for-git-users/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> jj (Jujutsu) is a Git-compatible version control system with some interesting ideas — automatic change tracking, universal undo, and a different take on history editing. It works on top of your existing Git repos, so you can try it without committing to anything.&lt;/p>
&lt;hr>
&lt;h2 id="why-im-trying-it">
 Why I&amp;rsquo;m trying it
 &lt;a class="heading-link" href="#why-im-trying-it">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The &lt;a href="https://github.com/jj-vcs/jj/releases/tag/v0.39.0" class="external-link" target="_blank" rel="noopener">v0.39.0 release&lt;/a> hit &lt;a href="https://news.ycombinator.com/item?id=47301499" class="external-link" target="_blank" rel="noopener">Hacker News&lt;/a> and I finally decided to give it a proper go. I&amp;rsquo;ve been a happy Git user for years — worktrees, rebases, filter-branch rewrites, the lot. I&amp;rsquo;m not looking to replace Git, but jj takes some interesting approaches to things like history editing and conflict handling that I wanted to explore.&lt;/p></description></item><item><title>Why Jetson Nano Still Matters in 2026</title><link>https://brtkwr.com/posts/2026-03-06-why-jetson-nano-still-matters-in-2026/</link><pubDate>Fri, 06 Mar 2026 15:55:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-06-why-jetson-nano-still-matters-in-2026/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> Jetson Nano is old and stuck on Ubuntu 18.04-era software, but it&amp;rsquo;s still a great always-on host for bounded edge workloads like OpenClaw. Treat it like an appliance, not a modern dev workstation.&lt;/p>
&lt;p>Frank Kelly, who also owns a Jetson Nano, asked my OpenClaw bot to write a blog post about it as a test. It politely replied that it would draft something and wait for my approval before posting. That first draft turned into this post after some back-and-forth in the Telegram group chat.&lt;/p></description></item><item><title>Mutating webhooks and ghost affinity on StatefulSets</title><link>https://brtkwr.com/posts/2026-03-06-mutating-webhooks-and-ghost-affinity-on-statefulsets/</link><pubDate>Fri, 06 Mar 2026 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-06-mutating-webhooks-and-ghost-affinity-on-statefulsets/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Mutating admission webhooks that inject scheduling rules (nodeSelector, tolerations, affinity) persist on StatefulSet pods even after you clean the StatefulSet template. The webhook re-fires on every pod CREATE and can read stale metadata to re-inject what you removed. The fix is to scale to 0 and back up — rollout restart doesn&amp;rsquo;t work.&lt;/p>
&lt;hr>
&lt;p>I was decommissioning a GKE &lt;a href="./posts/2025-11-19-gke-computeclass-cost-optimisation/" >ComputeClass&lt;/a>. ComputeClasses let you define scheduling profiles — node selectors, tolerations, affinity rules — and apply them to namespaces via labels. Under the hood, GKE enforces them with a mutating admission webhook (&lt;code>warden-mutating.config.common-webhooks.networking.gke.io&lt;/code>).&lt;/p></description></item><item><title>Bulk cleaning stale git worktrees and branches</title><link>https://brtkwr.com/posts/2026-03-06-bulk-cleaning-stale-git-worktrees/</link><pubDate>Fri, 06 Mar 2026 05:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-06-bulk-cleaning-stale-git-worktrees/</guid><description>&lt;p>I use &lt;a href="./posts/2025-07-21-git-worktrees-for-parallel-development/" >git worktrees&lt;/a> heavily for parallel development. One worktree per ticket, across dozens of repositories. They&amp;rsquo;re especially useful if you work with AI coding agents — each agent gets its own isolated worktree, so it can run tests, install dependencies, and make changes without stepping on your work or another agent&amp;rsquo;s. The downside is that worktrees accumulate fast. I ended up with 256 of them consuming 28GB of disk, 700+ stale local branches, and 70% of it all referencing branches that were merged months ago.&lt;/p></description></item><item><title>Rewriting Git History with an LLM for Conventional Commits</title><link>https://brtkwr.com/posts/2026-03-02-rewriting-git-history-with-llm-conventional-commits/</link><pubDate>Mon, 02 Mar 2026 20:21:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-02-rewriting-git-history-with-llm-conventional-commits/</guid><description>&lt;p>&lt;strong>TL;DR:&lt;/strong> Feed your entire &lt;code>git log&lt;/code> + file lists into a single LLM call to generate a bash hash map of conventional commit messages, then apply it with &lt;code>git filter-branch&lt;/code> in seconds. 143 commits rewritten in 6 seconds, one API call, ~$0.05.&lt;/p>
&lt;hr>
&lt;h2 id="why-bother">
 Why bother?
 &lt;a class="heading-link" href="#why-bother">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Good commit messages are documentation you get for free — but only if they&amp;rsquo;re actually meaningful. My repo had months of &lt;code>auto: update 2026-03-01T14:00:02&lt;/code> from a dumb cron job, mixed with inconsistently worded agent-written messages. Running &lt;code>git log&lt;/code> was useless. I couldn&amp;rsquo;t grep for feature additions, distinguish fixes from docs changes, or understand what happened on any given day without reading diffs.&lt;/p></description></item><item><title>Upgrading OpenClaw to Latest on Jetson Nano with Node 22</title><link>https://brtkwr.com/posts/2026-03-02-upgrading-openclaw-to-latest-node22-on-jetson-nano/</link><pubDate>Mon, 02 Mar 2026 06:45:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-03-02-upgrading-openclaw-to-latest-node22-on-jetson-nano/</guid><description>&lt;p>This is a follow-up to my original post, &lt;a href="./posts/2026-02-08-installing-openclaw-on-jetson-nano/" >Installing OpenClaw on a Jetson Nano&lt;/a>, where I got things working with Bun on Ubuntu 18.04. That setup ran fine for a few weeks — until I tried to upgrade.&lt;/p>
&lt;h2 id="why-upgrade">
 Why upgrade?
 &lt;a class="heading-link" href="#why-upgrade">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The original Bun-based install (&lt;code>2026.2.6&lt;/code>) was working fine, but I wanted access to newer features — improved Telegram handling, cron job fixes, better model fallback chains, and the new adaptive thinking defaults for Claude. OpenClaw moves fast, and staying weeks behind means missing meaningful improvements.&lt;/p></description></item><item><title>Go containers and the OOM killer</title><link>https://brtkwr.com/posts/2026-02-24-go-containers-and-the-oom-killer/</link><pubDate>Tue, 24 Feb 2026 12:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-24-go-containers-and-the-oom-killer/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Go doesn&amp;rsquo;t auto-detect container memory limits. Without &lt;code>GOMEMLIMIT&lt;/code>, the GC lets the heap double freely until the OOM killer strikes. Read the cgroup limit at startup and set &lt;code>GOMEMLIMIT&lt;/code> to ~85% of it via an entrypoint script so it adapts automatically when VPA adjusts your limits.&lt;/p>
&lt;hr>
&lt;p>I was investigating a pod that had been crashlooping for 25 hours. The usual suspects — liveness probe failures, connection resets, CrashLoopBackOff. After deleting the pod and watching it come back healthy on a different node, I assumed it was a flaky spot instance. But looking closer, it was OOMing.&lt;/p></description></item><item><title>Stop re-running BigQuery queries when paginating</title><link>https://brtkwr.com/posts/2026-02-23-free-bigquery-pagination-with-job-reuse/</link><pubDate>Mon, 23 Feb 2026 22:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-23-free-bigquery-pagination-with-job-reuse/</guid><description>&lt;p>I recently learnt something about BigQuery pagination that I wish I&amp;rsquo;d known sooner. If you&amp;rsquo;re paginating BQ results with &lt;code>LIMIT&lt;/code>/&lt;code>OFFSET&lt;/code> in the SQL, you&amp;rsquo;re probably paying full slot cost on every single page request — even though BQ already has the results sitting there.&lt;/p>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>BigQuery has a &lt;a href="https://cloud.google.com/bigquery/docs/cached-results" class="external-link" target="_blank" rel="noopener">query cache&lt;/a>, but it only kicks in when the SQL text and parameters are identical. If &lt;code>OFFSET&lt;/code> changes on every page, every request is a cache miss.&lt;/p></description></item><item><title>Happy — controlling Claude Code from your phone</title><link>https://brtkwr.com/posts/2026-02-18-happy-controlling-claude-code-from-your-phone/</link><pubDate>Wed, 18 Feb 2026 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-18-happy-controlling-claude-code-from-your-phone/</guid><description>&lt;p>&lt;a href="https://happy.engineering/" class="external-link" target="_blank" rel="noopener">Happy&lt;/a> lets you control Claude Code sessions from your phone. You run &lt;code>happy&lt;/code> instead of &lt;code>claude&lt;/code>, scan a QR code, and your session shows up on your phone. I&amp;rsquo;ve been using it for a few days — approving permissions on the go, watching output while making coffee, or talking to it with &lt;a href="https://happy.engineering/docs/features/" class="external-link" target="_blank" rel="noopener">voice input&lt;/a>. It&amp;rsquo;s open source (MIT) and free to use — there&amp;rsquo;s an optional £19.99/month subscription that gets you early access to new features.&lt;/p></description></item><item><title>How Cloudflare proxy mode silently breaks SendGrid email delivery</title><link>https://brtkwr.com/posts/2026-02-16-how-cloudflare-proxy-mode-silently-breaks-sendgrid-email-delivery/</link><pubDate>Mon, 16 Feb 2026 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-16-how-cloudflare-proxy-mode-silently-breaks-sendgrid-email-delivery/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: If you manage SendGrid DNS records in Cloudflare, make sure PTR-related records are set to DNS-only. Proxying them silently breaks reverse DNS verification and causes email delivery failures with no alerts or warnings.&lt;/p>
&lt;hr>
&lt;h2 id="the-setup">
 The setup
 &lt;a class="heading-link" href="#the-setup">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>If you use SendGrid with a dedicated IP, you&amp;rsquo;ll have a PTR record so receiving mail servers can verify your identity through reverse DNS.&lt;/p>
&lt;p>The way this works: when a mail server receives a connection from your IP, it does a reverse lookup (&lt;code>dig -x &amp;lt;your-ip&amp;gt;&lt;/code>) to get a hostname, then a forward lookup on that hostname to check the IP matches. This is called FCrDNS — forward-confirmed reverse DNS. If it doesn&amp;rsquo;t match, the server assumes you&amp;rsquo;re suspicious and rejects or defers your email.&lt;/p></description></item><item><title>Switching from Poetry to uv</title><link>https://brtkwr.com/posts/2026-02-15-switching-from-poetry-to-uv/</link><pubDate>Sun, 15 Feb 2026 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-15-switching-from-poetry-to-uv/</guid><description>&lt;p>I migrated a few Python projects from Poetry to uv. The conversion is mostly mechanical, so this focuses on what changed and why it was worth doing.&lt;/p>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>uv is generally faster than Poetry for dependency resolution and installation&lt;/li>
&lt;li>Private registry auth goes from &amp;ldquo;install a keyring plugin&amp;rdquo; to &amp;ldquo;mount a credentials file&amp;rdquo;&lt;/li>
&lt;li>Your &lt;code>pyproject.toml&lt;/code> moves to PEP 621 standard format — no more &lt;code>[tool.poetry]&lt;/code>&lt;/li>
&lt;li>Use &lt;code>&amp;gt;=&lt;/code> lower bounds in &lt;code>pyproject.toml&lt;/code>, let the lockfile handle exact pinning&lt;/li>
&lt;/ul>
&lt;h2 id="why-switch">
 Why switch
 &lt;a class="heading-link" href="#why-switch">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>&lt;strong>Speed.&lt;/strong> Poetry&amp;rsquo;s resolver can be slow. In Docker builds, the &lt;code>poetry install&lt;/code> layer often took 60–90 seconds. With uv, the same layer is usually much faster. uv is written in Rust, resolves in parallel, and downloads packages concurrently.&lt;/p></description></item><item><title>Kubernetes health probes for stateful Python services</title><link>https://brtkwr.com/posts/2026-02-13-kubernetes-health-probes/</link><pubDate>Fri, 13 Feb 2026 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-13-kubernetes-health-probes/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: If your entrypoint script doesn&amp;rsquo;t use &lt;code>exec&lt;/code>, SIGTERM never reaches your Python app and graceful shutdown silently does nothing. Docker compose masks this entirely.&lt;/p>
&lt;hr>
&lt;p>I use a single &lt;code>/health&lt;/code> endpoint for all three Kubernetes probes — startup, liveness, and readiness. The difference in behaviour comes from &lt;code>failureThreshold&lt;/code> in the probe config, not from separate code paths.&lt;/p>
&lt;h2 id="one-endpoint-three-probes">
 One endpoint, three probes
 &lt;a class="heading-link" href="#one-endpoint-three-probes">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The key insight is that &lt;code>failureThreshold&lt;/code> controls how tolerant each probe is. All three probes hit the same &lt;code>/health&lt;/code> endpoint, but they react differently to failures:&lt;/p></description></item><item><title>Fixing slow Docker builds on ephemeral CI runners</title><link>https://brtkwr.com/posts/2026-02-12-fixing-slow-docker-builds-on-ephemeral-ci-runners/</link><pubDate>Thu, 12 Feb 2026 22:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-12-fixing-slow-docker-builds-on-ephemeral-ci-runners/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: &lt;code>--mount=type=cache&lt;/code> makes &lt;code>RUN&lt;/code> layers non-deterministic. On ephemeral runners the mount is always empty, so BuildKit can&amp;rsquo;t match layers from registry cache. Removing cache mounts, switching to registry cache with dynamic fallback, gating exports to deploy branches, and restricting triggers dropped builds from ~27 min to ~2 min — and cut redundant builds entirely.&lt;/p>
&lt;hr>
&lt;p>I&amp;rsquo;d been ignoring slow Docker builds on a project for a while — around 27 minutes per build on ephemeral GCP runners, most of that spent in &lt;code>uv sync&lt;/code> downloading Python dependencies from scratch. Every single build. Even though BuildKit caching was configured.&lt;/p></description></item><item><title>SSH fallback hosts with ProxyCommand</title><link>https://brtkwr.com/posts/2026-02-09-ssh-fallback-hosts-with-proxycommand/</link><pubDate>Mon, 09 Feb 2026 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-09-ssh-fallback-hosts-with-proxycommand/</guid><description>&lt;p>I have a &lt;a href="./tags/jetson-nano" >Jetson Nano&lt;/a> at home that I SSH into from my laptop. At home it&amp;rsquo;s on a local IP, but when I&amp;rsquo;m out I reach it via a public IP. I got tired of switching between &lt;code>ssh jetson-home&lt;/code> and &lt;code>ssh jetson-www&lt;/code> depending on where I am.&lt;/p>
&lt;p>A VPN like Tailscale or WireGuard would also solve this, but I don&amp;rsquo;t always remember to switch it on. I wanted something that just works without thinking about it.&lt;/p></description></item><item><title>Installing OpenClaw on a Jetson Nano</title><link>https://brtkwr.com/posts/2026-02-08-installing-openclaw-on-jetson-nano/</link><pubDate>Sun, 08 Feb 2026 21:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-08-installing-openclaw-on-jetson-nano/</guid><description>&lt;p>The idea of messaging an AI assistant from my phone while I&amp;rsquo;m out walking and having it write code that I can steer — &amp;ldquo;try this approach instead&amp;rdquo;, &amp;ldquo;add tests for that edge case&amp;rdquo;, &amp;ldquo;actually scrap that, let&amp;rsquo;s do X&amp;rdquo; — is genuinely exciting. &lt;a href="https://openclaw.ai" class="external-link" target="_blank" rel="noopener">OpenClaw&lt;/a> makes this possible by bridging Telegram (or WhatsApp) to Claude Code, so you can kick off and guide coding sessions from anywhere.&lt;/p>
&lt;p>I wouldn&amp;rsquo;t run it on my main laptop though. OpenClaw gets broad system access — filesystem, shell, network, credentials — and there&amp;rsquo;s been a steady stream of security issues, including a &lt;a href="https://security.utoronto.ca/advisories/openclaw-vulnerability-notification/" class="external-link" target="_blank" rel="noopener">one-click RCE vulnerability&lt;/a> (CVE-2026-25253) and &lt;a href="https://jfrog.com/blog/giving-openclaw-the-keys-to-your-kingdom-read-this-first/" class="external-link" target="_blank" rel="noopener">hundreds of exposed instances&lt;/a> found via Shodan with no authentication. I had a Jetson Nano lying around doing nothing, so I used that instead — if something goes wrong, the blast radius is limited to a cheap ARM board.&lt;/p></description></item><item><title>Fixing HDMI resolution on a Jetson Nano</title><link>https://brtkwr.com/posts/2026-02-08-fixing-hdmi-resolution-on-jetson-nano/</link><pubDate>Sun, 08 Feb 2026 20:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-08-fixing-hdmi-resolution-on-jetson-nano/</guid><description>&lt;p>I connected my Jetson Nano to an external projector and the console text was microscopic. The framebuffer was running at 3840x2160 (4K) on a display where I could barely read anything. Here&amp;rsquo;s how I fixed it.&lt;/p>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The Jetson Nano auto-negotiates resolution via EDID when an HDMI display is connected. If your monitor or projector supports 4K, it&amp;rsquo;ll default to 4K. On a console TTY with the default &lt;code>8x16&lt;/code> font, that means tiny, unreadable text.&lt;/p></description></item><item><title>Running HPA and VPA together on Kubernetes</title><link>https://brtkwr.com/posts/2026-02-07-running-hpa-and-vpa-together-on-kubernetes/</link><pubDate>Sat, 07 Feb 2026 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-07-running-hpa-and-vpa-together-on-kubernetes/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: HPA handles horizontal scaling on CPU, VPA right-sizes memory. Split their concerns with &lt;code>controlledResources: [&amp;quot;memory&amp;quot;]&lt;/code> so they don&amp;rsquo;t fight. Drop CPU limits. Match memory requests to limits for Guaranteed QoS. Only create PDBs when you have 2+ replicas. Don&amp;rsquo;t run HPA in staging.&lt;/p>
&lt;hr>
&lt;p>I&amp;rsquo;ve been writing about autoscaling on GKE for a while now. It started with &lt;a href="./posts/2025-10-15-debugging-kubernetes-hpa-scaling/" >debugging HPA scaling&lt;/a>, then understanding the &lt;a href="./posts/2025-10-21-gke-cluster-autoscaler/" >four layers of GKE autoscaling&lt;/a>, then &lt;a href="./posts/2025-11-19-gke-computeclass-cost-optimisation/" >optimising node costs with ComputeClass&lt;/a>, and most recently hitting the &lt;a href="./posts/2026-02-01-getting-vpa-resourcepolicy-right-in-helm-charts/" >VPA resourcePolicy trap&lt;/a> when templating VPA in Helm.&lt;/p></description></item><item><title>Batch updating files across GitHub repos without cloning</title><link>https://brtkwr.com/posts/2026-02-06-batch-updating-files-across-github-repos-without-cloning/</link><pubDate>Fri, 06 Feb 2026 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-06-batch-updating-files-across-github-repos-without-cloning/</guid><description>&lt;p>I needed to roll out the same GitHub Actions workflow change across a bunch of repositories. Doing it repo by repo via clone-edit-commit-push sounded painful.&lt;/p>
&lt;p>Turns out you can read, modify, and commit files directly through the GitHub API without ever cloning a repo. The technique works for any file, but workflow YAML is where I&amp;rsquo;ve found it most useful — shared CI config that lives in every repo and needs to stay in sync. I&amp;rsquo;d recommend doing the whole thing in Python rather than shell — the base64 encoding, JSON payloads, and regex transformations are much less painful than trying to wrangle them with bash quoting.&lt;/p></description></item><item><title>Pulling Twilio Usage Data into Google Sheets</title><link>https://brtkwr.com/posts/2026-02-04-twilio-usage-google-sheets-apps-script/</link><pubDate>Wed, 04 Feb 2026 09:45:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-04-twilio-usage-google-sheets-apps-script/</guid><description>&lt;p>I needed to track daily Twilio costs in a spreadsheet. The Twilio console has usage data but no easy export, and I wanted it updating automatically.&lt;/p>
&lt;h2 id="the-solution-apps-script--secret-manager">
 The solution: Apps Script + Secret Manager
 &lt;a class="heading-link" href="#the-solution-apps-script--secret-manager">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I wrote an Apps Script that:&lt;/p>
&lt;ul>
&lt;li>Fetches Twilio credentials from GCP Secret Manager (not hardcoded)&lt;/li>
&lt;li>Pulls daily usage data via the Twilio API&lt;/li>
&lt;li>Only fetches missing dates (incremental updates)&lt;/li>
&lt;li>Handles pagination automatically&lt;/li>
&lt;/ul>
&lt;p>The full script is here: &lt;a href="https://gist.github.com/brtkwr/e6b5432df190dbfe9a45f2daee4b16cb" class="external-link" target="_blank" rel="noopener">twilio-usage-appscript&lt;/a>&lt;/p></description></item><item><title>Using Google Sheets API with gcloud ADC</title><link>https://brtkwr.com/posts/2026-02-04-google-sheets-api-with-gcloud-adc/</link><pubDate>Wed, 04 Feb 2026 09:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-04-google-sheets-api-with-gcloud-adc/</guid><description>&lt;p>I wanted to pull data from a Google Sheet using &lt;code>curl&lt;/code> and my existing gcloud credentials. Should be simple, right?&lt;/p>
&lt;h2 id="the-naive-approach-doesnt-work">
 The naive approach (doesn&amp;rsquo;t work)
 &lt;a class="heading-link" href="#the-naive-approach-doesnt-work">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud auth application-default login &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --scopes&lt;span class="o">=&lt;/span>https://www.googleapis.com/auth/spreadsheets,https://www.googleapis.com/auth/cloud-platform
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl &lt;span class="s2">&amp;#34;https://sheets.googleapis.com/v4/spreadsheets/YOUR_SPREADSHEET_ID&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="k">$(&lt;/span>gcloud auth application-default print-access-token&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This gets you the dreaded &amp;ldquo;app is blocked&amp;rdquo; screen:&lt;/p>
&lt;p>&lt;img src="./images/gcloud-adc-app-blocked.png" alt="This app is blocked">&lt;/p>
&lt;p>The problem is that Google Workspace APIs (Sheets, Drive, etc.) are &amp;ldquo;sensitive scopes&amp;rdquo; and the default gcloud OAuth client isn&amp;rsquo;t verified to use them.&lt;/p></description></item><item><title>Getting VPA resourcePolicy right in Helm charts</title><link>https://brtkwr.com/posts/2026-02-01-getting-vpa-resourcepolicy-right-in-helm-charts/</link><pubDate>Sun, 01 Feb 2026 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-02-01-getting-vpa-resourcepolicy-right-in-helm-charts/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Templating VPA &lt;code>minAllowed&lt;/code> from deployment resource requests prevents VPA from reducing resources. Popular charts make these fields optional and static. After fixing this in our charts: 93% CPU reduction on one service (200m → 15m).&lt;/p>
&lt;hr>
&lt;p>I was debugging why our pods were still over-provisioned after months of VPA running in &lt;code>Initial&lt;/code> mode. VPA recommendations looked reasonable - suggesting 15m CPU when we were requesting 200m - but nothing was changing.&lt;/p></description></item><item><title>gcloud auth application-default login for kubectl</title><link>https://brtkwr.com/posts/2026-01-30-gcloud-auth-application-default-login-for-kubectl/</link><pubDate>Fri, 30 Jan 2026 09:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-30-gcloud-auth-application-default-login-for-kubectl/</guid><description>&lt;p>For four years I thought kubectl required &lt;code>gcloud auth login&lt;/code>. But I also needed ADC for scripts and apps, so every time my tokens expired I&amp;rsquo;d authenticate twice - once for the CLI, then again with &lt;code>gcloud auth application-default login&lt;/code>. Two browser windows, two OAuth flows, same Google account.&lt;/p>
&lt;p>Yes, &lt;code>gcloud auth login --update-adc&lt;/code> exists, but it doesn&amp;rsquo;t let you specify scopes. I needed spreadsheets access for some scripts.&lt;/p>
&lt;p>Turns out you can make kubectl use ADC directly. One auth with custom scopes, everything works.&lt;/p></description></item><item><title>Pruning Claude Code conversation history</title><link>https://brtkwr.com/posts/2026-01-22-pruning-claude-code-conversation-history/</link><pubDate>Thu, 22 Jan 2026 09:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-22-pruning-claude-code-conversation-history/</guid><description>&lt;p>A few days ago I &lt;a href="./posts/2026-01-18-using-claude-to-free-200gb-from-a-full-disk/" >cleaned up 200GB&lt;/a> from my Mac and deliberately kept my Claude Code history at 2.3GB. Four days later it had grown to 9.5GB. Here&amp;rsquo;s how I pruned it back to 1.1GB without breaking conversation continuity.&lt;/p>
&lt;p>This also broke &lt;a href="./posts/2026-01-05-ccs-fuzzy-finder-for-claude-code-conversations/" >ccs&lt;/a> - my fuzzy finder for Claude conversations. Parsing 3GB JSONL files was taking forever. I&amp;rsquo;ve since added &lt;code>--max-size&lt;/code> filtering to ccs (defaults to 1GB), but that just hides the problem. Better to prune the files themselves.&lt;/p></description></item><item><title>Extracting Travel Data from macOS Photos Library</title><link>https://brtkwr.com/posts/2026-01-18-extract-travel-data-macos-photos/</link><pubDate>Sun, 18 Jan 2026 13:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-18-extract-travel-data-macos-photos/</guid><description>&lt;p>I was updating my &lt;a href="./about/" >about page&lt;/a> and wanted to add a timeline of places I&amp;rsquo;ve travelled to. I&amp;rsquo;ve got thousands of geotagged photos in my Photos library spanning over a decade, and manually going through them would&amp;rsquo;ve been painful. Here&amp;rsquo;s how I extracted the data using Python and SQLite.&lt;/p>
&lt;h2 id="the-challenge">
 The Challenge
 &lt;a class="heading-link" href="#the-challenge">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Photos.app doesn&amp;rsquo;t have a built-in way to export a list of countries from geotagged photos. I needed to:&lt;/p></description></item><item><title>Using Claude to free 200GB from a nearly full disk</title><link>https://brtkwr.com/posts/2026-01-18-using-claude-to-free-200gb-from-a-full-disk/</link><pubDate>Sun, 18 Jan 2026 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-18-using-claude-to-free-200gb-from-a-full-disk/</guid><description>&lt;p>My Mac was close to 100% full. macOS was complaining and I couldn&amp;rsquo;t install updates. Here&amp;rsquo;s how I used Claude to analyse my disk usage and safely free 200GB.&lt;/p>
&lt;h2 id="system-specs">
 System specs
 &lt;a class="heading-link" href="#system-specs">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Machine&lt;/strong>: MacBook Pro M4 Pro (24GB RAM)&lt;/li>
&lt;li>&lt;strong>Disk&lt;/strong>: 494GB SSD&lt;/li>
&lt;li>&lt;strong>macOS&lt;/strong>: 26.1 (developer preview)&lt;/li>
&lt;li>&lt;strong>Before&lt;/strong>: 487GB used (98.5% full, 7GB free)&lt;/li>
&lt;li>&lt;strong>After&lt;/strong>: 274GB used (55% full, 220GB free)&lt;/li>
&lt;/ul>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Freed 200GB by cleaning:&lt;/p></description></item><item><title>Making bumpver play nice with uv</title><link>https://brtkwr.com/posts/2026-01-14-bumpver-with-uv/</link><pubDate>Wed, 14 Jan 2026 15:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-14-bumpver-with-uv/</guid><description>&lt;p>I was setting up automated version bumps for a Python project using &lt;a href="https://github.com/mbarkhau/bumpver" class="external-link" target="_blank" rel="noopener">bumpver&lt;/a> and &lt;a href="https://docs.astral.sh/uv/" class="external-link" target="_blank" rel="noopener">uv&lt;/a>. The problem: when bumpver updates the version in &lt;code>pyproject.toml&lt;/code>, the &lt;code>uv.lock&lt;/code> file also needs updating - and those changes need to be included in the same commit.&lt;/p>
&lt;h2 id="the-naive-approach">
 The naive approach
 &lt;a class="heading-link" href="#the-naive-approach">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>My first thought was to use bumpver&amp;rsquo;s &lt;code>pre_commit_hook&lt;/code> to run &lt;code>uv lock&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-toml" data-lang="toml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># bumpver.toml&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">[&lt;/span>&lt;span class="nx">tool&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bumpver&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">current_version&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;26.01.14&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">version_pattern&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;0Y.0M.0D[-INC0]&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">commit&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">tag&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">push&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nx">pre_commit_hook&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;uv lock&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This fails immediately - bumpver expects a &lt;strong>path to a script&lt;/strong>, not a command:&lt;/p></description></item><item><title>Migrating from a GitHub bot user to a GitHub App</title><link>https://brtkwr.com/posts/2026-01-06-migrating-from-github-bot-user-to-github-app/</link><pubDate>Tue, 06 Jan 2026 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-06-migrating-from-github-bot-user-to-github-app/</guid><description>&lt;p>We had a dedicated &amp;ldquo;bot&amp;rdquo; user account for GitHub automation - creating PRs, pushing commits, merging branches. It worked, but it always felt like a hack. The account consumed a licence seat, used a long-lived PAT that someone had to rotate manually, and the audit trail was confusing because it looked like a real person.&lt;/p>
&lt;p>I finally migrated everything to a GitHub App. This touched 30+ repositories and 6 custom GitHub Actions across the org. Here&amp;rsquo;s the full process.&lt;/p></description></item><item><title>CCS: Search for Claude Code conversations</title><link>https://brtkwr.com/posts/2026-01-05-ccs-fuzzy-finder-for-claude-code-conversations/</link><pubDate>Mon, 05 Jan 2026 00:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2026-01-05-ccs-fuzzy-finder-for-claude-code-conversations/</guid><description>&lt;p>I&amp;rsquo;ve accumulated hundreds of Claude Code sessions and kept losing track of where I solved specific problems. The built-in &lt;code>/resume&lt;/code> shows recent sessions, but anything older than a few days? Gone.&lt;/p>
&lt;p>So I built CCS (Claude Code Search) to fix that.&lt;/p>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install agentic-utils/tap/ccs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ccs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Type to search, Enter to resume. That&amp;rsquo;s it.&lt;/p>
&lt;script src="https://asciinema.org/a/JXHQVf8PGBG2Orsl.js" id="asciicast-JXHQVf8PGBG2Orsl" async="true">&lt;/script>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Claude Code stores conversations in &lt;code>~/.claude/projects/&lt;/code> as JSONL files. Each session gets its own file:&lt;/p></description></item><item><title>My Neovim setup in 2025</title><link>https://brtkwr.com/posts/2025-12-28-my-neovim-setup/</link><pubDate>Sun, 28 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-28-my-neovim-setup/</guid><description>&lt;p>I&amp;rsquo;ve been using Neovim full-time for a couple of years now. Here&amp;rsquo;s my current setup - built on LazyVim with a focus on Python development using the Astral toolchain.&lt;/p>
&lt;h2 id="why-lazyvim">
 Why LazyVim
 &lt;a class="heading-link" href="#why-lazyvim">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I started with a custom config but maintaining it became tedious. &lt;a href="https://www.lazyvim.org/" class="external-link" target="_blank" rel="noopener">LazyVim&lt;/a> gives me sensible defaults while still allowing customisation. The key benefits:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Batteries included&lt;/strong> - LSP, treesitter, telescope all preconfigured&lt;/li>
&lt;li>&lt;strong>Lazy loading&lt;/strong> - plugins load on demand, keeping startup fast&lt;/li>
&lt;li>&lt;strong>Easy updates&lt;/strong> - &lt;code>LazyVim&lt;/code> handles plugin compatibility&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">~/.config/nvim/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── init.lua # Bootstraps lazy.nvim
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── lazy-lock.json # Lockfile for reproducible installs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── lua/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├── config/ # Options, keymaps, autocmds
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └── plugins/ # Custom plugin configs
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="python-setup-ty--ruff">
 Python setup: ty + ruff
 &lt;a class="heading-link" href="#python-setup-ty--ruff">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The interesting part of my config is the Python tooling. I&amp;rsquo;ve switched from pyright to &lt;a href="https://github.com/astral-sh/ty" class="external-link" target="_blank" rel="noopener">ty&lt;/a> - Astral&amp;rsquo;s new type checker (the same folks behind ruff and uv).&lt;/p></description></item><item><title>Python tools I used in 2025</title><link>https://brtkwr.com/posts/2025-12-27-python-tools-i-used-in-2025/</link><pubDate>Sat, 27 Dec 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-27-python-tools-i-used-in-2025/</guid><description>&lt;p>I&amp;rsquo;ve overhauled my Python development setup over the past year. The ecosystem has improved dramatically - faster tools, better defaults, and less configuration. This post describes my current workflow and links to the detailed posts on each tool.&lt;/p>
&lt;h2 id="the-stack">
 The stack
 &lt;a class="heading-link" href="#the-stack">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Purpose&lt;/th>
 &lt;th>Tool&lt;/th>
 &lt;th>Replaces&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Package management&lt;/td>
 &lt;td>uv&lt;/td>
 &lt;td>pip, pip-tools, poetry, virtualenv&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Linting &amp;amp; formatting&lt;/td>
 &lt;td>ruff&lt;/td>
 &lt;td>flake8, black, isort, pylint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Type checking&lt;/td>
 &lt;td>pyright / ty&lt;/td>
 &lt;td>mypy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Testing&lt;/td>
 &lt;td>pytest&lt;/td>
 &lt;td>unittest&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pre-commit&lt;/td>
 &lt;td>pre-commit + ruff&lt;/td>
 &lt;td>manual linting&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>The theme: &lt;strong>fewer tools, each doing more&lt;/strong>. uv replaced four tools. Ruff replaced three. Less config, fewer version conflicts, faster CI.&lt;/p></description></item><item><title>Kubernetes debugging in 2025</title><link>https://brtkwr.com/posts/2025-12-27-kubernetes-debugging-in-2025/</link><pubDate>Sat, 27 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-27-kubernetes-debugging-in-2025/</guid><description>&lt;p>My Kubernetes workflow changed a lot this year. The biggest shift: I started using LLMs as a debugging partner. Combined with a few CLI tools I can&amp;rsquo;t live without, debugging got noticeably faster.&lt;/p>
&lt;h2 id="aliases-that-stuck">
 Aliases that stuck
 &lt;a class="heading-link" href="#aliases-that-stuck">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>First, the obvious ones:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">k&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;kubectl&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">alias&lt;/span> &lt;span class="nv">kdrain&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;kubectl drain --ignore-daemonsets --delete-emptydir-data&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>k&lt;/code> saves thousands of keystrokes a year. &lt;code>kdrain&lt;/code> saves me from forgetting the flags every time I need to drain a node.&lt;/p></description></item><item><title>Developing Magento plugins on Kubernetes with git-sync</title><link>https://brtkwr.com/posts/2025-12-22-operating-magento-on-kubernetes/</link><pubDate>Mon, 22 Dec 2025 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-22-operating-magento-on-kubernetes/</guid><description>&lt;p>This is Part 2 covering plugin development. &lt;a href="./posts/2025-12-18-deploying-magento-to-kubernetes/" >Part 1&lt;/a> covers setting up Magento on Kubernetes.&lt;/p>
&lt;p>With Magento running on Kubernetes, you can use git-sync sidecars for live plugin development - push to your repo and see changes within 60 seconds. This post covers the git-sync setup, Magento CLI quirks you&amp;rsquo;ll encounter, and debugging tips.&lt;/p>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Git-sync: use &lt;code>--link&lt;/code> mode to handle worktree hash changes automatically&lt;/li>
&lt;li>Git-sync: create symlinks in both init and postStart (OOMKill safety)&lt;/li>
&lt;li>Git-sync: consider &lt;code>--exechook&lt;/code> for auto cache flush on code changes&lt;/li>
&lt;li>Core setup belongs in init containers (blocking), customizations in postStart (async)&lt;/li>
&lt;li>Bug #35751: &lt;code>design/theme/theme_id&lt;/code> can&amp;rsquo;t be set via CLI for store scope&lt;/li>
&lt;li>Run &lt;code>app:config:import&lt;/code> before &lt;code>config:set&lt;/code> to avoid &amp;ldquo;command unavailable&amp;rdquo; errors&lt;/li>
&lt;/ul>
&lt;h2 id="magento-cli-quirks">
 Magento CLI quirks
 &lt;a class="heading-link" href="#magento-cli-quirks">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;h3 id="bug-35751-store-level-theme-config-fails">
 Bug #35751: Store-level theme config fails
 &lt;a class="heading-link" href="#bug-35751-store-level-theme-config-fails">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h3>
&lt;p>Setting a theme for a specific store view should be simple:&lt;/p></description></item><item><title>Deploying Magento to Kubernetes</title><link>https://brtkwr.com/posts/2025-12-18-deploying-magento-to-kubernetes/</link><pubDate>Thu, 18 Dec 2025 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-18-deploying-magento-to-kubernetes/</guid><description>&lt;p>This guide covers deploying Magento 2.4.8 to Kubernetes with MariaDB and OpenSearch as sidecars.&lt;/p>
&lt;p>The key benefit: &lt;strong>disposable environments&lt;/strong>. Database corrupted? Config in a weird state? Just delete the PVC and redeploy - you&amp;rsquo;re back to a clean Magento install with sample data in under 5 minutes. No more debugging broken dev environments.&lt;/p>
&lt;p>&lt;a href="./posts/2025-12-22-operating-magento-on-kubernetes/" >Part 2&lt;/a> covers developing Magento plugins using git-sync for live code updates.&lt;/p>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Don&amp;rsquo;t wait for sidecars in postStart hooks (deadlock)&lt;/li>
&lt;li>Magento 2.4.8 dropped Elasticsearch 7 support entirely&lt;/li>
&lt;li>Use startup probes with generous timeouts&lt;/li>
&lt;li>Run &lt;code>bin/magento&lt;/code> commands as www-data from the start, not just chown at the end&lt;/li>
&lt;li>Configure SSL offloader header or you&amp;rsquo;ll get redirect loops&lt;/li>
&lt;li>Enable store codes for multi-store URL paths&lt;/li>
&lt;li>Reload Apache after DI compile to clear opcache&lt;/li>
&lt;li>Sample data needs &lt;code>catalog:images:resize&lt;/code> after copying media files&lt;/li>
&lt;li>Or just use my &lt;a href="https://github.com/brtkwr/magento-helm" class="external-link" target="_blank" rel="noopener">Helm chart&lt;/a> and skip the pain&lt;/li>
&lt;/ul>
&lt;h2 id="the-architecture">
 The architecture
 &lt;a class="heading-link" href="#the-architecture">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>This setup uses a Magento image with a couple of proprietary extensions. The deployment is a single pod with five containers plus two init containers.&lt;/p></description></item><item><title>Git worktree helper with fzf</title><link>https://brtkwr.com/posts/2025-12-17-git-worktree-fzf-helper/</link><pubDate>Wed, 17 Dec 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-17-git-worktree-fzf-helper/</guid><description>&lt;p>I use git worktrees constantly for parallel development, but typing &lt;code>git worktree list&lt;/code>, copying the path, and &lt;code>cd&lt;/code>-ing got tedious. So I wrote a shell function.&lt;/p>
&lt;h2 id="the-function">
 The function
 &lt;a class="heading-link" href="#the-function">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Add this to your &lt;code>.zshrc&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Git worktree selector with fzf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">create&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">delete&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">copy_envrc&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">false&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">target&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> _wt_usage&lt;span class="o">()&lt;/span> &lt;span class="o">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> cat &lt;span class="s">&amp;lt;&amp;lt;USAGE
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">Usage: wt [-c &amp;lt;name&amp;gt;] [-e] [-d [path]] [-h]
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">Options:
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -c &amp;lt;name&amp;gt; Create a new worktree with branch name &amp;lt;name&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -e Copy .envrc from root and run direnv allow (use with -c)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -d [path] Delete a worktree (fuzzy select if no path given, use &amp;#39;.&amp;#39; for current)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s"> -h Show this help message
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s">USAGE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="nv">$#&lt;/span> -gt &lt;span class="m">0&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">case&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> in
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -c&lt;span class="o">)&lt;/span> &lt;span class="nv">create&lt;/span>&lt;span class="o">=&lt;/span>true&lt;span class="p">;&lt;/span> &lt;span class="nv">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$2&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="nb">shift&lt;/span> &lt;span class="m">2&lt;/span> &lt;span class="p">;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -e&lt;span class="o">)&lt;/span> &lt;span class="nv">copy_envrc&lt;/span>&lt;span class="o">=&lt;/span>true&lt;span class="p">;&lt;/span> &lt;span class="nb">shift&lt;/span> &lt;span class="p">;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -d&lt;span class="o">)&lt;/span> &lt;span class="nv">delete&lt;/span>&lt;span class="o">=&lt;/span>true&lt;span class="p">;&lt;/span> shift&lt;span class="p">;&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="nv">$#&lt;/span> -gt &lt;span class="m">0&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> ! &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span>~ ^- &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nv">target&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> shift&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span> &lt;span class="p">;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> -h&lt;span class="o">)&lt;/span> _wt_usage&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="p">;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> *&lt;span class="o">)&lt;/span> _wt_usage&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span> &lt;span class="p">;;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">esac&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">(&lt;/span>&lt;span class="nv">$create&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nv">$copy_envrc&lt;/span>&lt;span class="o">)&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nv">$delete&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Error: -c/-e and -d are mutually exclusive&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nv">$create&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -z &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$name&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">{&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Error: -c requires a name&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> 1&lt;span class="p">;&lt;/span> &lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">root&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>git rev-parse --show-toplevel&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">new_path&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$root&lt;/span>&lt;span class="s2">/&lt;/span>&lt;span class="nv">$name&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> git worktree add &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$new_path&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -b &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$name&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$new_path&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="nv">$copy_envrc&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="o">[[&lt;/span> -f &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$root&lt;/span>&lt;span class="s2">/.envrc&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> cp &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$root&lt;/span>&lt;span class="s2">/.envrc&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$new_path&lt;/span>&lt;span class="s2">/.envrc&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> direnv allow
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">elif&lt;/span> &lt;span class="nv">$delete&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> to_delete
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$target&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">to_delete&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>realpath &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$target&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nv">to_delete&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>git worktree list &lt;span class="p">|&lt;/span> fzf --height 40% --reverse &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $1}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -z &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$to_delete&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">main_wt&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>git worktree list &lt;span class="p">|&lt;/span> head -1 &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $1}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$to_delete&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$main_wt&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;Error: cannot delete main worktree&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="m">1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>realpath .&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$to_delete&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>* &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$main_wt&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> git worktree remove &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$to_delete&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">local&lt;/span> &lt;span class="nv">selected&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>git worktree list &lt;span class="p">|&lt;/span> fzf --height 40% --reverse &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $1}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="o">[[&lt;/span> -n &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$selected&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">]]&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">cd&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$selected&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="usage">
 Usage
 &lt;a class="heading-link" href="#usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Fuzzy select and cd to a worktree&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create a new worktree with branch name&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt -c feature-x
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create worktree and setup direnv&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt -c feature-x -e
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete a worktree (fuzzy select)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt -d
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Delete current worktree&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">wt -d .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="why--e-matters">
 Why -e matters
 &lt;a class="heading-link" href="#why--e-matters">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I use direnv for environment variables in each repo. Without &lt;code>-e&lt;/code>, I&amp;rsquo;d have to manually copy &lt;code>.envrc&lt;/code> and run &lt;code>direnv allow&lt;/code> every time I create a worktree. Now it&amp;rsquo;s automatic.&lt;/p></description></item><item><title>Cleaning up Docker</title><link>https://brtkwr.com/posts/2025-12-16-cleaning-up-docker/</link><pubDate>Tue, 16 Dec 2025 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-16-cleaning-up-docker/</guid><description>&lt;p>My disk was filling up with old Docker images. I got a low disk space warning and traced 80GB back to Docker. Here&amp;rsquo;s how to reclaim space.&lt;/p>
&lt;h2 id="how-much-space-can-you-reclaim">
 How much space can you reclaim?
 &lt;a class="heading-link" href="#how-much-space-can-you-reclaim">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>First, see what Docker is using:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker system df
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This shows you a breakdown by images, containers, and volumes. In my case:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">TYPE TOTAL ACTIVE SIZE RECLAIMABLE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Images 47 12 35.6GB 28.4GB (79%)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Containers 8 3 127.3MB 84.2MB (66%)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Local Volumes 15 2 2.1GB 1.8GB (85%)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Build Cache 0 0 0B 0B
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Nearly 30GB reclaimable. Worth the cleanup.&lt;/p></description></item><item><title>Deploying to Cloudflare Pages with wrangler</title><link>https://brtkwr.com/posts/2025-12-14-wrangler-for-cloudflare-pages/</link><pubDate>Sun, 14 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-14-wrangler-for-cloudflare-pages/</guid><description>&lt;p>I was deploying a SvelteKit app to Cloudflare Pages and wanted to do it from the command line rather than connecting to GitHub. Wrangler makes this straightforward.&lt;/p>
&lt;h2 id="why-cloudflare-pages-over-vercelnetlify">
 Why Cloudflare Pages over Vercel/Netlify
 &lt;a class="heading-link" href="#why-cloudflare-pages-over-vercelnetlify">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I evaluated all three for hosting a SvelteKit app:&lt;/p>
&lt;p>&lt;strong>Vercel&lt;/strong> - Excellent Next.js integration, but I found the pricing confusing for non-hobby projects. The free tier is generous, but the jump to Pro felt steep for what we needed.&lt;/p></description></item><item><title>Silencing alerts properly in Alertmanager</title><link>https://brtkwr.com/posts/2025-12-10-alertmanager-silences/</link><pubDate>Wed, 10 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-10-alertmanager-silences/</guid><description>&lt;p>I was doing planned maintenance on a Kubernetes cluster. Within seconds of starting the work, my phone was buzzing with PagerDuty alerts. I needed to silence them, but not blindly - only the specific alerts related to the maintenance work.&lt;/p>
&lt;h2 id="via-the-ui">
 Via the UI
 &lt;a class="heading-link" href="#via-the-ui">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Go to &lt;code>http://alertmanager:9093/#/silences&lt;/code> and click &amp;ldquo;New Silence&amp;rdquo;.&lt;/p>
&lt;p>The UI is actually good for this - you can see which alerts will match your silence before creating it. The UI works well for one-off silences and &lt;code>amtool&lt;/code> for scripted ones.&lt;/p></description></item><item><title>Fix gcloud PATH in shell</title><link>https://brtkwr.com/posts/2025-12-09-fix-gcloud-path-in-shell/</link><pubDate>Tue, 09 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-09-fix-gcloud-path-in-shell/</guid><description>&lt;p>After installing gcloud, the &lt;code>gcloud&lt;/code> command wasn&amp;rsquo;t found. The PATH wasn&amp;rsquo;t set up properly. This happened after I upgraded macOS and my shell config got reset.&lt;/p>
&lt;p>Check if gcloud is installed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ls ~/google-cloud-sdk/bin/gcloud
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Add to your shell config (&lt;code>~/.zshrc&lt;/code> or &lt;code>~/.bashrc&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Google Cloud SDK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f ~/google-cloud-sdk/path.zsh.inc &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">source&lt;/span> ~/google-cloud-sdk/path.zsh.inc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="o">[&lt;/span> -f ~/google-cloud-sdk/completion.zsh.inc &lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="k">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">source&lt;/span> ~/google-cloud-sdk/completion.zsh.inc
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">fi&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reload your shell:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">source&lt;/span> ~/.zshrc
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or just manually add the path:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$HOME&lt;/span>&lt;span class="s2">/google-cloud-sdk/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Check it works:&lt;/p></description></item><item><title>OpenSSL certificate testing with SNI</title><link>https://brtkwr.com/posts/2025-12-04-openssl-certificate-testing-with-sni/</link><pubDate>Thu, 04 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-04-openssl-certificate-testing-with-sni/</guid><description>&lt;p>I was debugging why HTTPS requests to &lt;code>app.example.com&lt;/code> were failing with certificate errors. The certificate on the server was valid, but clients were getting &amp;ldquo;certificate name mismatch&amp;rdquo; errors. Turns out the server hosted multiple domains on the same IP, and I was getting the wrong certificate. SNI solved it.&lt;/p>
&lt;h2 id="what-is-sni">
 What is SNI?
 &lt;a class="heading-link" href="#what-is-sni">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Server Name Indication (SNI) is a TLS extension that lets a server host multiple SSL certificates on a single IP address. The client sends the hostname in the initial TLS handshake, and the server responds with the correct certificate.&lt;/p></description></item><item><title>GitHub PR commands with gh</title><link>https://brtkwr.com/posts/2025-12-01-gh-pr-commands/</link><pubDate>Mon, 01 Dec 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-01-gh-pr-commands/</guid><description>&lt;p>Using &lt;code>gh&lt;/code> for PR work is much faster than the web UI, and allows staying in the terminal. It eliminates the need to constantly reach for the browser to check PR status, view diffs, or create new PRs.&lt;/p>
&lt;p>View the current PR in browser:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr view --web
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create a PR:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr create
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Create a PR against a specific base branch:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh pr create --base main
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>View PR diff:&lt;/p></description></item><item><title>Advent of Code tips</title><link>https://brtkwr.com/posts/2025-12-01-advent-of-code-tips/</link><pubDate>Mon, 01 Dec 2025 06:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-12-01-advent-of-code-tips/</guid><description>&lt;p>I started Advent of Code 2025. Here are some tips I&amp;rsquo;ve picked up.&lt;/p>
&lt;h2 id="my-solving-workflow">
 My solving workflow
 &lt;a class="heading-link" href="#my-solving-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ol>
&lt;li>Read the problem twice - I often miss details on first read&lt;/li>
&lt;li>Work through the example by hand (pen and paper for complex ones)&lt;/li>
&lt;li>Write the naive solution first&lt;/li>
&lt;li>If it&amp;rsquo;s too slow, then optimise&lt;/li>
&lt;/ol>
&lt;p>I&amp;rsquo;ve wasted time optimising prematurely. Get it working, then make it fast.&lt;/p></description></item><item><title>Accessing secrets in GKE</title><link>https://brtkwr.com/posts/2025-11-29-accessing-gcloud-secrets/</link><pubDate>Sat, 29 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-29-accessing-gcloud-secrets/</guid><description>&lt;p>I needed to check what was in a GCP secret to debug why an application wasn&amp;rsquo;t starting. Here&amp;rsquo;s how we inject secrets into GKE workloads, plus some CLI commands for debugging.&lt;/p>
&lt;h2 id="debugging-secrets-with-gcloud">
 Debugging secrets with gcloud
 &lt;a class="heading-link" href="#debugging-secrets-with-gcloud">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>These commands are useful for checking secret values during debugging - not how you&amp;rsquo;d access them in production.&lt;/p>
&lt;p>List all secrets:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud secrets list
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Filter by name:&lt;/p></description></item><item><title>Viewing Docker container logs</title><link>https://brtkwr.com/posts/2025-11-27-viewing-docker-container-logs/</link><pubDate>Thu, 27 Nov 2025 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-27-viewing-docker-container-logs/</guid><description>&lt;p>I left a container running over a long weekend and came back to find it had consumed 50GB of disk space with logs. The application was logging every request at DEBUG level. That&amp;rsquo;s when I learnt about log rotation and the &lt;code>--since&lt;/code> flag.&lt;/p>
&lt;h2 id="basic-log-viewing">
 Basic log viewing
 &lt;a class="heading-link" href="#basic-log-viewing">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>View logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs my-container
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Follow logs in real time:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker logs my-container -f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Search through logs:&lt;/p></description></item><item><title>Getting your public IP with curl</title><link>https://brtkwr.com/posts/2025-11-26-curl-ifconfig-for-public-ip/</link><pubDate>Wed, 26 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-26-curl-ifconfig-for-public-ip/</guid><description>&lt;p>I needed to find my public IP to add it to a firewall allowlist.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl ifconfig.me
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That&amp;rsquo;s it. Returns just your IP address.&lt;/p>
&lt;h2 id="why-you-need-to-check-your-public-ip">
 Why you need to check your public IP
 &lt;a class="heading-link" href="#why-you-need-to-check-your-public-ip">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Your local machine&amp;rsquo;s IP address (like &lt;code>192.168.1.x&lt;/code> or &lt;code>10.0.0.x&lt;/code>) is different from your public IP. Your public IP is what the internet sees when you make requests.&lt;/p>
&lt;p>Common reasons I need to check it:&lt;/p></description></item><item><title>PostgreSQL WAL monitoring</title><link>https://brtkwr.com/posts/2025-11-25-postgresql-wal-monitoring/</link><pubDate>Tue, 25 Nov 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-25-postgresql-wal-monitoring/</guid><description>&lt;p>At 2am, I got an alert: database disk usage at 85%. Logged in, checked the usual suspects (tables, indexes, temp files), and everything looked normal. Then I checked &lt;code>/var/lib/postgresql/data/pg_wal/&lt;/code> and found 40GB of Write-Ahead Logs. That&amp;rsquo;s when I learned to monitor WAL properly.&lt;/p>
&lt;h2 id="what-triggered-wal-monitoring">
 What triggered WAL monitoring?
 &lt;a class="heading-link" href="#what-triggered-wal-monitoring">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The incident that made me care about WAL was a replica that fell behind during a large data import. PostgreSQL kept accumulating WAL files because it couldn&amp;rsquo;t confirm the replica had received them. Disk filled up, writes stopped, production went down. Good times.&lt;/p></description></item><item><title>Connecting to Cloud SQL with gcloud</title><link>https://brtkwr.com/posts/2025-11-25-connecting-to-cloud-sql-with-gcloud/</link><pubDate>Tue, 25 Nov 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-25-connecting-to-cloud-sql-with-gcloud/</guid><description>&lt;p>I needed to connect to a Cloud SQL instance to debug a data issue. Here&amp;rsquo;s the quick way to get a psql session without fiddling with IP allowlists or VPN connections.&lt;/p>
&lt;p>Using a service account:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud beta sql connect my-instance &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --project my-project &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --user&lt;span class="o">=&lt;/span>my-user &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d&lt;span class="o">=&lt;/span>my-database
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Using IAM authentication (your Google account):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud sql generate-login-token &lt;span class="p">|&lt;/span> pbcopy &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span>gcloud beta sql connect my-instance &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --project my-project &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --user&lt;span class="o">=&lt;/span>me@example.com &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d&lt;span class="o">=&lt;/span>my-database
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>pbcopy&lt;/code> puts the token in your clipboard so you can paste it as the password.&lt;/p></description></item><item><title>Running local stacks with Docker Compose</title><link>https://brtkwr.com/posts/2025-11-24-running-local-stacks-with-docker-compose/</link><pubDate>Mon, 24 Nov 2025 16:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-24-running-local-stacks-with-docker-compose/</guid><description>&lt;p>I use Docker Compose for local multi-service environments because it keeps runtime config in one file and makes startup/teardown repeatable.&lt;/p>
&lt;h2 id="my-typical-docker-composeyml-structure">
 My typical docker-compose.yml structure
 &lt;a class="heading-link" href="#my-typical-docker-composeyml-structure">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Here&amp;rsquo;s what a typical development setup looks like for me:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">postgres&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:16&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">localdev&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_DB&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">myapp&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;5432:5432&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">postgres_data:/var/lib/postgresql/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthcheck&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;CMD-SHELL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pg_isready -U postgres&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redis:7-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;6379:6379&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">redis_data:/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">postgres_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I always include health checks for databases - they prevent your application from trying to connect before the database is ready. Learnt that the hard way after debugging race conditions on startup.&lt;/p></description></item><item><title>Searching code across GitHub with gh</title><link>https://brtkwr.com/posts/2025-11-24-searching-code-across-github-with-gh/</link><pubDate>Mon, 24 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-24-searching-code-across-github-with-gh/</guid><description>&lt;p>I needed to find all the places in our GitHub org that were using a deprecated GitHub Actions runner label. We were migrating from &lt;code>runs-on: self-hosted&lt;/code> to &lt;code>runs-on: ${{ vars.RUNNER_STANDARD }}&lt;/code>, and I needed to find every workflow file that still used the old pattern. The &lt;code>gh search code&lt;/code> command is perfect for this kind of organisation-wide search.&lt;/p>
&lt;p>Basic search:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gh search code &lt;span class="s1">&amp;#39;pattern&amp;#39;&lt;/span> --owner my-org
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Search in a specific path:&lt;/p></description></item><item><title>Troubleshoot stuck ArgoCD syncs</title><link>https://brtkwr.com/posts/2025-11-20-force-and-troubleshoot-argocd-syncs/</link><pubDate>Thu, 20 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-20-force-and-troubleshoot-argocd-syncs/</guid><description>&lt;p>I was debugging why a Kubernetes deployment wasn&amp;rsquo;t updating after merging a PR. The manifest was in git, but the cluster was still running the old version. Turns out ArgoCD was stuck in a sync loop. Here&amp;rsquo;s what I learned.&lt;/p>
&lt;h2 id="my-gitops-workflow">
 My GitOps workflow
 &lt;a class="heading-link" href="#my-gitops-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>A typical ArgoCD workflow looks like this:&lt;/p>
&lt;ol>
&lt;li>Open PR with manifest changes (Helm chart, kustomize, or raw YAML)&lt;/li>
&lt;li>Merge to main&lt;/li>
&lt;li>ArgoCD detects the change (polls every 3 minutes by default)&lt;/li>
&lt;li>Auto-sync deploys to cluster (if enabled)&lt;/li>
&lt;/ol>
&lt;p>When auto-sync is disabled, I manually trigger syncs. This is common for production environments where you want explicit control.&lt;/p></description></item><item><title>Web scraping with Selenium: quick setup</title><link>https://brtkwr.com/posts/2025-11-19-web-scraping-with-selenium-quick-setup/</link><pubDate>Wed, 19 Nov 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-19-web-scraping-with-selenium-quick-setup/</guid><description>&lt;p>I needed to scrape property listings from a real estate site that heavily relied on JavaScript to load content. The page source was empty until React rendered everything, so traditional tools like &lt;code>requests&lt;/code> + BeautifulSoup wouldn&amp;rsquo;t work. Selenium was the answer.&lt;/p>
&lt;h2 id="what-i-was-scraping">
 What I was scraping
 &lt;a class="heading-link" href="#what-i-was-scraping">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Property data from a site that dynamically loaded listings as you scrolled. I needed to:&lt;/p>
&lt;ol>
&lt;li>Load the page&lt;/li>
&lt;li>Scroll to trigger lazy loading&lt;/li>
&lt;li>Wait for listings to appear&lt;/li>
&lt;li>Extract the data&lt;/li>
&lt;/ol>
&lt;p>The site had no public API, and while I could have reverse-engineered their internal API calls, using Selenium meant I didn&amp;rsquo;t have to worry about authentication or rate limiting logic.&lt;/p></description></item><item><title>Cost-optimising GKE with ComputeClass</title><link>https://brtkwr.com/posts/2025-11-19-gke-computeclass-cost-optimisation/</link><pubDate>Wed, 19 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-19-gke-computeclass-cost-optimisation/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: I built &lt;a href="https://github.com/brtkwr/gkecc" class="external-link" target="_blank" rel="noopener">gkecc&lt;/a> to generate cost-optimised GKE ComputeClass specs from live pricing data. It sorts by total cost (CPU + RAM), not just CPU, and intelligently interleaves spot and on-demand instances.&lt;/p>
&lt;hr>
&lt;p>I was manually maintaining ComputeClass specs for our GKE clusters. Every few months I&amp;rsquo;d check GCP pricing docs, update the priority order, and hope I got it right. It was tedious and error-prone.&lt;/p>
&lt;p>The breaking point: I discovered our &amp;ldquo;cost-optimised&amp;rdquo; spec was recommending instances that were actually 30% more expensive than alternatives. The problem? I&amp;rsquo;d been sorting by CPU price alone, ignoring RAM pricing entirely.&lt;/p></description></item><item><title>direnv Python path issues</title><link>https://brtkwr.com/posts/2025-11-19-direnv-python-path-issues/</link><pubDate>Wed, 19 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-19-direnv-python-path-issues/</guid><description>&lt;p>I was setting up a new Python project with direnv to manage environment variables. Suddenly &lt;code>python&lt;/code> wasn&amp;rsquo;t found, even though it was definitely installed. Here&amp;rsquo;s what happened and how I fixed it.&lt;/p>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I&amp;rsquo;d been using direnv happily for months, managing API keys and secrets per project. Then I added a custom PATH in my &lt;code>.envrc&lt;/code> and broke everything:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># This was the problem&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">export&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>/some/custom/path
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Running &lt;code>which python&lt;/code> returned nothing. Even &lt;code>python3&lt;/code> was gone. My shell couldn&amp;rsquo;t find Python at all.&lt;/p></description></item><item><title>Content Security Policy headers</title><link>https://brtkwr.com/posts/2025-11-18-content-security-policy-headers/</link><pubDate>Tue, 18 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-18-content-security-policy-headers/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Start with &lt;code>default-src 'self'&lt;/code> and add exceptions as needed. Use report-only mode first to avoid breaking your site.&lt;/p>
&lt;hr>
&lt;p>I added CSP to a production site recently and immediately broke half of it. Inline scripts stopped working, external fonts disappeared, analytics failed silently. Here&amp;rsquo;s what I learned debugging it.&lt;/p>
&lt;h2 id="a-reasonable-starter-csp">
 A reasonable starter CSP
 &lt;a class="heading-link" href="#a-reasonable-starter-csp">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>This is a solid starting point for most projects:&lt;/p></description></item><item><title>pytest-asyncio mode configuration</title><link>https://brtkwr.com/posts/2025-11-17-pytest-asyncio-mode/</link><pubDate>Mon, 17 Nov 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-17-pytest-asyncio-mode/</guid><description>&lt;p>I was getting warnings about pytest-asyncio decorators. Here&amp;rsquo;s the fix.&lt;/p>
&lt;h2 id="why-async-tests-are-tricky">
 Why async tests are tricky
 &lt;a class="heading-link" href="#why-async-tests-are-tricky">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Async tests need an event loop to run. pytest-asyncio handles this, but there are gotchas:&lt;/p>
&lt;ul>
&lt;li>Each test gets its own event loop by default&lt;/li>
&lt;li>Fixtures need to match the test&amp;rsquo;s async-ness&lt;/li>
&lt;li>Session-scoped async fixtures are fiddly&lt;/li>
&lt;/ul>
&lt;p>The old way (deprecated):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@pytest.mark.asyncio&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">async&lt;/span> &lt;span class="k">def&lt;/span> &lt;span class="nf">test_something&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">await&lt;/span> &lt;span class="n">some_async_function&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">result&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">expected&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The warning says to configure &lt;code>asyncio_mode&lt;/code> instead.&lt;/p></description></item><item><title>Pydantic v2 migration tips</title><link>https://brtkwr.com/posts/2025-11-17-pydantic-v2-migration-tips/</link><pubDate>Mon, 17 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-17-pydantic-v2-migration-tips/</guid><description>&lt;p>Pydantic v2 is a ground-up rewrite with a Rust core, and it shows - validation is noticeably faster. But the migration isn&amp;rsquo;t trivial. I spent about a day migrating a medium-sized FastAPI project, and here&amp;rsquo;s what I learned.&lt;/p>
&lt;h2 id="use-bump-pydantic-first">
 Use bump-pydantic first
 &lt;a class="heading-link" href="#use-bump-pydantic-first">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Before doing anything manual, run the automated migration tool:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install bump-pydantic
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">bump-pydantic .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This handles about 80% of the changes automatically. It renames methods, updates imports, and converts the obvious stuff. Review the diff carefully though - it occasionally makes mistakes with complex validators.&lt;/p></description></item><item><title>Migrating from Redocly to Docusaurus</title><link>https://brtkwr.com/posts/2025-11-12-docusaurus-setup/</link><pubDate>Wed, 12 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-12-docusaurus-setup/</guid><description>&lt;p>I was evaluating Docusaurus as a replacement for our Redocly docs site. We had around 200 documentation pages and needed more flexibility than Redocly could offer. Turns out Docusaurus was exactly what we needed.&lt;/p>
&lt;h2 id="why-docusaurus-over-other-doc-tools">
 Why Docusaurus over other doc tools
 &lt;a class="heading-link" href="#why-docusaurus-over-other-doc-tools">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I looked at a few options:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Redocly&lt;/strong> - Excellent for pure OpenAPI reference docs, but limited customisation and no good way to add interactive examples&lt;/li>
&lt;li>&lt;strong>GitBook&lt;/strong> - Nice UI but expensive for teams, and you&amp;rsquo;re locked into their platform&lt;/li>
&lt;li>&lt;strong>VitePress&lt;/strong> - Very fast but Vue-focused, and we already had React components we wanted to reuse&lt;/li>
&lt;li>&lt;strong>Nextra&lt;/strong> - Good Next.js integration, but Docusaurus has better versioning and plugin ecosystem&lt;/li>
&lt;li>&lt;strong>MkDocs&lt;/strong> - Python-based, which would have meant learning Jinja templates&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Docusaurus won because:&lt;/strong>&lt;/p></description></item><item><title>FastAPI patterns for production APIs</title><link>https://brtkwr.com/posts/2025-11-08-fastapi-patterns-for-production-apis/</link><pubDate>Sat, 08 Nov 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-11-08-fastapi-patterns-for-production-apis/</guid><description>&lt;p>FastAPI has a few defaults that make API services easier to maintain: request validation, typed contracts, and built-in docs. These are the patterns I reuse most.&lt;/p>
&lt;h2 id="quick-start">
 Quick start
 &lt;a class="heading-link" href="#quick-start">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">pip install fastapi uvicorn
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">fastapi&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">FastAPI&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">app&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">FastAPI&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@app.get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;/items/&lt;/span>&lt;span class="si">{item_id}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">read_item&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">item_id&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">int&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">q&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="nb">str&lt;/span> &lt;span class="o">|&lt;/span> &lt;span class="kc">None&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kc">None&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="s2">&amp;#34;item_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">item_id&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;q&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="n">q&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">uvicorn main:app --reload
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That &lt;code>item_id: int&lt;/code> type hint isn&amp;rsquo;t just documentation - FastAPI validates it. Hit &lt;code>/items/abc&lt;/code> and you get a 422 with a clear error message. No more manual type checking.&lt;/p></description></item><item><title>GKE cluster autoscaler</title><link>https://brtkwr.com/posts/2025-10-21-gke-cluster-autoscaler/</link><pubDate>Tue, 21 Oct 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-21-gke-cluster-autoscaler/</guid><description>&lt;p>GKE has four layers of autoscaling that work together: HPA, VPA, cluster autoscaler, and NAP. Understanding how they interact saves a lot of debugging time.&lt;/p>
&lt;h2 id="the-four-layers">
 The four layers
 &lt;a class="heading-link" href="#the-four-layers">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>HPA&lt;/strong> - scales pods horizontally (more replicas) based on CPU/memory/custom metrics&lt;/li>
&lt;li>&lt;strong>VPA&lt;/strong> - scales pods vertically (bigger requests/limits) based on actual usage&lt;/li>
&lt;li>&lt;strong>Cluster Autoscaler&lt;/strong> - scales existing node pools when pods are pending&lt;/li>
&lt;li>&lt;strong>NAP (Node Auto Provisioning)&lt;/strong> - creates new node pools when no existing pool fits&lt;/li>
&lt;/ol>
&lt;p>HPA and VPA solve different problems: HPA for handling more traffic, VPA for right-sizing resource requests. Don&amp;rsquo;t use both on CPU/memory for the same workload - they&amp;rsquo;ll fight. VPA is great for workloads where you don&amp;rsquo;t know the right resource requests upfront.&lt;/p></description></item><item><title>Sentry DSN configuration</title><link>https://brtkwr.com/posts/2025-10-21-sentry-dsn-configuration/</link><pubDate>Tue, 21 Oct 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-21-sentry-dsn-configuration/</guid><description>&lt;p>I was setting up Sentry for a new Python service. The first time I ran it locally without a DSN configured, the app crashed at startup. Sentry&amp;rsquo;s SDK doesn&amp;rsquo;t gracefully handle missing configuration. Here&amp;rsquo;s what I learned about setting it up properly.&lt;/p>
&lt;h2 id="the-problem">
 The problem
 &lt;a class="heading-link" href="#the-problem">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Sentry throws errors if initialised without a valid DSN. This breaks local development unless everyone has a Sentry DSN configured (spoiler: they won&amp;rsquo;t).&lt;/p></description></item><item><title>Debug Helm charts before deploy with helm template</title><link>https://brtkwr.com/posts/2025-10-17-debug-helm-charts-with-helm-template/</link><pubDate>Fri, 17 Oct 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-17-debug-helm-charts-with-helm-template/</guid><description>&lt;p>I was trying to figure out why my Helm chart wasn&amp;rsquo;t rendering correctly. A missing &lt;code>if&lt;/code> condition meant a ConfigMap wasn&amp;rsquo;t being created in production. &lt;code>helm template&lt;/code> showed me exactly what was (and wasn&amp;rsquo;t) being generated.&lt;/p>
&lt;h2 id="basic-usage">
 Basic usage
 &lt;a class="heading-link" href="#basic-usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>See what Helm will generate without installing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm template my-release ./my-chart
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Show extra rendering details:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm template my-release ./my-chart --debug
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With values files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">helm template my-release ./my-chart -f values.yaml -f values-prod.yaml
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="show-specific-templates">
 Show specific templates
 &lt;a class="heading-link" href="#show-specific-templates">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Only render one template:&lt;/p></description></item><item><title>Debugging Kubernetes HPA scaling</title><link>https://brtkwr.com/posts/2025-10-15-debugging-kubernetes-hpa-scaling/</link><pubDate>Wed, 15 Oct 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-15-debugging-kubernetes-hpa-scaling/</guid><description>&lt;p>I&amp;rsquo;d been using HPAs for a while without really understanding them. They worked, so I never looked closely. Then I noticed a service was running way more replicas than the load justified — and I realised I didn&amp;rsquo;t actually know how to read what HPA was doing or why. Here&amp;rsquo;s what I learned debugging it.&lt;/p>
&lt;h2 id="what-i-saw">
 What I saw
 &lt;a class="heading-link" href="#what-i-saw">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl get hpa
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">my-app Deployment/my-app 124%/70% 1 10 6 45m
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Six replicas for a service that barely had any traffic. The utilisation was showing 124%, which seemed wrong — the pods weren&amp;rsquo;t under heavy load at all.&lt;/p></description></item><item><title>Kubernetes tolerations and node selectors</title><link>https://brtkwr.com/posts/2025-10-12-kubernetes-tolerations/</link><pubDate>Sun, 12 Oct 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-12-kubernetes-tolerations/</guid><description>&lt;p>Pods were stuck in Pending and I couldn&amp;rsquo;t figure out why. &lt;code>kubectl describe&lt;/code> showed &amp;ldquo;0/12 nodes are available: 12 node(s) had taint that the pod didn&amp;rsquo;t tolerate.&amp;rdquo; I&amp;rsquo;d added spot instance nodes to the cluster but forgot they come pre-tainted. Once I added the right toleration, everything scheduled immediately.&lt;/p>
&lt;h2 id="real-use-cases">
 Real use cases
 &lt;a class="heading-link" href="#real-use-cases">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Common use cases for taints include:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>GPU nodes&lt;/strong> - &lt;code>gpu=true:NoSchedule&lt;/code> so only ML workloads land there&lt;/li>
&lt;li>&lt;strong>Spot/preemptible nodes&lt;/strong> - &lt;code>cloud.google.com/gke-spot=true:NoSchedule&lt;/code> for batch jobs that can handle interruption&lt;/li>
&lt;li>&lt;strong>High-memory nodes&lt;/strong> - &lt;code>workload=data-processing:NoSchedule&lt;/code> for memory-intensive jobs&lt;/li>
&lt;/ul>
&lt;p>The pattern is: taint nodes with special characteristics, then add tolerations to pods that should use them.&lt;/p></description></item><item><title>Querying GCP logs with gcloud</title><link>https://brtkwr.com/posts/2025-10-11-querying-gcp-logs-with-gcloud/</link><pubDate>Sat, 11 Oct 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-11-querying-gcp-logs-with-gcloud/</guid><description>&lt;p>I needed to search Cloud Logging for specific events from the command line. The CLI is much faster than clicking through the Console when you know what you&amp;rsquo;re looking for.&lt;/p>
&lt;h2 id="basic-query">
 Basic query
 &lt;a class="heading-link" href="#basic-query">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud logging &lt;span class="nb">read&lt;/span> &lt;span class="s1">&amp;#39;resource.type=&amp;#34;cloud_function&amp;#34;&amp;#39;&lt;/span> --project&lt;span class="o">=&lt;/span>my-project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="filter-by-time">
 Filter by time
 &lt;a class="heading-link" href="#filter-by-time">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud logging &lt;span class="nb">read&lt;/span> &lt;span class="s1">&amp;#39;timestamp&amp;gt;=&amp;#34;2025-10-11T10:00:00Z&amp;#34; timestamp&amp;lt;=&amp;#34;2025-10-11T11:00:00Z&amp;#34;&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --project&lt;span class="o">=&lt;/span>my-project
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="filter-by-resource">
 Filter by resource
 &lt;a class="heading-link" href="#filter-by-resource">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Cloud Functions:&lt;/p></description></item><item><title>Keeping Homebrew packages up to date</title><link>https://brtkwr.com/posts/2025-10-10-brew-upgrade/</link><pubDate>Fri, 10 Oct 2025 16:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-10-brew-upgrade/</guid><description>&lt;p>I hadn&amp;rsquo;t run &lt;code>brew upgrade&lt;/code> in a couple of months and suddenly &lt;code>gh&lt;/code> started failing with cryptic errors. Turned out I was three major versions behind. Now I upgrade weekly - it&amp;rsquo;s less painful than debugging version mismatches.&lt;/p>
&lt;h2 id="basic-commands">
 Basic commands
 &lt;a class="heading-link" href="#basic-commands">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Update Homebrew itself:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Upgrade all packages:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew upgrade
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Upgrade a specific package:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew upgrade gh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>See what&amp;rsquo;s outdated:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew outdated
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install a cask (GUI app):&lt;/p></description></item><item><title>Moving commits between branches with cherry-pick</title><link>https://brtkwr.com/posts/2025-10-10-git-cherry-pick/</link><pubDate>Fri, 10 Oct 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-10-git-cherry-pick/</guid><description>&lt;p>I&amp;rsquo;d been working on a feature branch and accidentally committed a bug fix to it instead of &lt;code>main&lt;/code>. The fix needed to go out now, but the feature branch had three other commits that weren&amp;rsquo;t ready. I didn&amp;rsquo;t want to merge the whole thing.&lt;/p>
&lt;h2 id="basic-usage">
 Basic usage
 &lt;a class="heading-link" href="#basic-usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Cherry-pick a commit:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git cherry-pick abc123
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If there are conflicts, resolve them and continue:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git cherry-pick --continue
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Or abort:&lt;/p></description></item><item><title>Replacing venv script workflows with uv run</title><link>https://brtkwr.com/posts/2025-10-10-replacing-venv-script-workflows-with-uv-run/</link><pubDate>Fri, 10 Oct 2025 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-10-10-replacing-venv-script-workflows-with-uv-run/</guid><description>&lt;p>When sharing quick Python scripts, the old workflow is usually: create a venv, install dependencies, and document setup steps. &lt;code>uv run&lt;/code> replaces that with a single command and script-local metadata.&lt;/p>
&lt;h2 id="inline-dependencies-the-killer-feature">
 Inline dependencies (the killer feature)
 &lt;a class="heading-link" href="#inline-dependencies-the-killer-feature">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>PEP 723 introduced a standard way to declare dependencies directly in a Python script using a special comment block:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># /// script&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># requires-python = &amp;#34;&amp;gt;=3.12&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># dependencies = [&amp;#34;requests&amp;#34;, &amp;#34;rich&amp;#34;]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># ///&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">requests&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">rich&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="nb">print&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">response&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;https://api.github.com&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">response&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">())&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>When you run this with &lt;code>uv run script.py&lt;/code>, uv reads the metadata, creates an isolated environment with those exact dependencies, and runs the script. No &lt;code>requirements.txt&lt;/code>, no &lt;code>pyproject.toml&lt;/code>, no venv activation. Just run it.&lt;/p></description></item><item><title>Debugging cert-manager in Kubernetes</title><link>https://brtkwr.com/posts/2025-09-20-debugging-cert-manager-in-kubernetes/</link><pubDate>Sat, 20 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-20-debugging-cert-manager-in-kubernetes/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: Certificate not ready? Check resources in this order: &lt;code>Certificate&lt;/code> → &lt;code>CertificateRequest&lt;/code> → &lt;code>Order&lt;/code> → &lt;code>Challenge&lt;/code>. The issue is usually at the Challenge level.&lt;/p>
&lt;hr>
&lt;p>Debugging cert-manager feels like archaeology. You start at the surface (Certificate) and dig down through layers of resources until you find where things went wrong. I&amp;rsquo;ve wasted hours staring at a Certificate stuck at &amp;ldquo;False&amp;rdquo; before learning this flow.&lt;/p>
&lt;h2 id="the-debugging-hierarchy">
 The debugging hierarchy
 &lt;a class="heading-link" href="#the-debugging-hierarchy">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>cert-manager creates a chain of resources when you request a certificate:&lt;/p></description></item><item><title>Setting up Dependabot for uv projects</title><link>https://brtkwr.com/posts/2025-09-15-dependabot-with-uv/</link><pubDate>Mon, 15 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-15-dependabot-with-uv/</guid><description>&lt;p>After migrating a project to uv, I noticed Dependabot PRs were still being created but CI was failing. Dependabot was updating &lt;code>pyproject.toml&lt;/code> but not regenerating &lt;code>uv.lock&lt;/code>, so the lockfile was out of sync. Here&amp;rsquo;s how I got it working.&lt;/p>
&lt;h2 id="basic-setup">
 Basic setup
 &lt;a class="heading-link" href="#basic-setup">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Create &lt;code>.github/dependabot.yml&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">updates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">package-ecosystem&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pip&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">directory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schedule&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;weekly&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">groups&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">python-packages&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">patterns&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>groups&lt;/code> section batches updates into single PRs - much better than 20 individual PRs.&lt;/p></description></item><item><title>Psql connection patterns I use for debugging</title><link>https://brtkwr.com/posts/2025-09-12-quick-psql-connection-patterns/</link><pubDate>Fri, 12 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-12-quick-psql-connection-patterns/</guid><description>&lt;p>I needed to connect to a PostgreSQL database for debugging production issues. After years of copying connection commands from Slack, I finally wrote down the patterns I actually use.&lt;/p>
&lt;h2 id="connection-methods">
 Connection methods
 &lt;a class="heading-link" href="#connection-methods">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Connect to localhost:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">psql -h localhost -U myuser -d mydb
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Connect to a specific port:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">psql -h localhost -p &lt;span class="m">5432&lt;/span> -U myuser -d mydb
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Connect via Cloud SQL proxy (commonly used in GCP environments - see &lt;a href="./posts/2025-11-25-connecting-to-cloud-sql-with-gcloud/" >Connecting to Cloud SQL with gcloud&lt;/a> for setup):&lt;/p></description></item><item><title>Shelving changes with git stash</title><link>https://brtkwr.com/posts/2025-09-08-git-stash/</link><pubDate>Mon, 08 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-08-git-stash/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: &lt;code>git stash&lt;/code> to save, &lt;code>git stash pop&lt;/code> to restore. Use &lt;code>-m &amp;quot;message&amp;quot;&lt;/code> if you&amp;rsquo;ll have multiple stashes.&lt;/p>
&lt;hr>
&lt;p>I was halfway through a refactor when someone pinged me about an urgent bug in production. I needed to context-switch immediately, but my working tree was a mess of half-finished changes. I couldn&amp;rsquo;t commit them (they didn&amp;rsquo;t even compile), and I didn&amp;rsquo;t want to lose them.&lt;/p>
&lt;h2 id="basic-workflow">
 Basic workflow
 &lt;a class="heading-link" href="#basic-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Stash current changes:&lt;/p></description></item><item><title>Debugging APIs with curl and jq</title><link>https://brtkwr.com/posts/2025-09-05-curl-with-jq/</link><pubDate>Fri, 05 Sep 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-05-curl-with-jq/</guid><description>&lt;p>I was debugging why a webhook wasn&amp;rsquo;t firing. The API was returning something, but staring at a wall of minified JSON in the terminal wasn&amp;rsquo;t helping. Piping to &lt;code>jq&lt;/code> made the problem obvious - a nested field I expected to be an object was actually &lt;code>null&lt;/code>.&lt;/p>
&lt;h2 id="basic-usage">
 Basic usage
 &lt;a class="heading-link" href="#basic-usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Basic request with formatted output:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl &lt;span class="s1">&amp;#39;https://api.example.com/data&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Quiet mode (no progress bar):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s1">&amp;#39;https://api.example.com/data&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Extract a specific field:&lt;/p></description></item><item><title>Undoing commits with git revert</title><link>https://brtkwr.com/posts/2025-09-04-git-revert/</link><pubDate>Thu, 04 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-04-git-revert/</guid><description>&lt;p>I once pushed a commit that broke production. The fix was already in progress but would take an hour. &lt;code>git revert&lt;/code> let me undo the damage immediately while keeping full history of what happened.&lt;/p>
&lt;h2 id="basic-usage">
 Basic usage
 &lt;a class="heading-link" href="#basic-usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Revert the last commit:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git revert HEAD
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Revert a specific commit:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git revert abc123
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This opens an editor for the commit message. Save and close to complete.&lt;/p></description></item><item><title>Setting up Claude CLI with MCP</title><link>https://brtkwr.com/posts/2025-09-02-setting-up-claude-cli-with-mcp/</link><pubDate>Tue, 02 Sep 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-02-setting-up-claude-cli-with-mcp/</guid><description>&lt;p>I wanted Claude to help me manage Linear tickets without leaving my terminal. The Model Context Protocol (MCP) makes this possible. Here&amp;rsquo;s how I set it up.&lt;/p>
&lt;h2 id="why-claude-cli-with-mcp">
 Why Claude CLI with MCP?
 &lt;a class="heading-link" href="#why-claude-cli-with-mcp">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>I spend most of my day in the terminal. Switching to a browser to check Linear tickets, update descriptions, or create new issues breaks my flow. With Claude CLI + MCP, I can:&lt;/p></description></item><item><title>Tmux session and pane commands</title><link>https://brtkwr.com/posts/2025-09-02-tmux-session-and-pane-commands/</link><pubDate>Tue, 02 Sep 2025 08:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-09-02-tmux-session-and-pane-commands/</guid><description>&lt;p>I started using tmux to keep terminal sessions running after disconnecting from SSH. Here&amp;rsquo;s what I use most.&lt;/p>
&lt;h2 id="quick-session-workflow">
 Quick session workflow
 &lt;a class="heading-link" href="#quick-session-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tmux new -s work
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># do work&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Ctrl+b d&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">tmux attach -t work
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Start a new session:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tmux
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Start a named session:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tmux new -s work
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Detach from session (keeps it running):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Ctrl+b d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>List sessions:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">tmux ls
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reattach to a session:&lt;/p></description></item><item><title>Managing tickets from the terminal with Linear CLI</title><link>https://brtkwr.com/posts/2025-08-07-linear-cli/</link><pubDate>Thu, 07 Aug 2025 16:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-07-linear-cli/</guid><description>&lt;p>I spend too much time switching between my terminal and Linear&amp;rsquo;s web app. The Linear CLI lets me manage tickets without leaving my editor. Here&amp;rsquo;s how I use it.&lt;/p>
&lt;h2 id="installation">
 Installation
 &lt;a class="heading-link" href="#installation">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Install via Homebrew:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">brew install schpet/tap/linear
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Authenticate (opens browser for OAuth):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">linear config
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This creates &lt;code>~/.config/linear/config.json&lt;/code> with your API token and default team/workspace.&lt;/p>
&lt;h2 id="my-ticket-workflow">
 My ticket workflow
 &lt;a class="heading-link" href="#my-ticket-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Here&amp;rsquo;s how I work with Linear tickets day-to-day:&lt;/p></description></item><item><title>Syncing files from GCS with gcloud storage</title><link>https://brtkwr.com/posts/2025-08-07-gcloud-storage-rsync/</link><pubDate>Thu, 07 Aug 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-07-gcloud-storage-rsync/</guid><description>&lt;p>I needed to download a bunch of files from a GCS bucket to process them locally. &lt;code>gcloud storage rsync&lt;/code> does the job, and it&amp;rsquo;s noticeably faster than the old &lt;code>gsutil&lt;/code> for large syncs.&lt;/p>
&lt;p>Sync a bucket to local directory:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud storage rsync -r gs://my-bucket/path/ ./local/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Exclude certain files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud storage rsync -r -x &lt;span class="s2">&amp;#34;.*\.tmp&lt;/span>$&lt;span class="s2">&amp;#34;&lt;/span> gs://my-bucket/path/ ./local/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Copy specific files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud storage cp gs://my-bucket/path/*.csv ./local/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Copy recursively:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">gcloud storage cp -r gs://my-bucket/path/ ./local/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The old &lt;code>gsutil&lt;/code> still works but &lt;code>gcloud storage&lt;/code> is the newer interface:&lt;/p></description></item><item><title>Alembic migration history</title><link>https://brtkwr.com/posts/2025-08-07-alembic-history/</link><pubDate>Thu, 07 Aug 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-07-alembic-history/</guid><description>&lt;p>I was investigating a production issue where a table column was missing. Turned out someone had merged a migration that dropped it without updating the application code. This is where knowing your way around Alembic&amp;rsquo;s history commands becomes essential.&lt;/p>
&lt;h2 id="basic-history-commands">
 Basic history commands
 &lt;a class="heading-link" href="#basic-history-commands">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Show migration history:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">alembic &lt;span class="nb">history&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This dumps the entire migration tree, which can be overwhelming on older projects. I rarely use this without filtering.&lt;/p></description></item><item><title>DNS lookups with dig</title><link>https://brtkwr.com/posts/2025-08-06-dns-lookups-with-dig/</link><pubDate>Wed, 06 Aug 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-06-dns-lookups-with-dig/</guid><description>&lt;p>When an endpoint times out, I check DNS early to rule it in or out quickly. &lt;code>dig&lt;/code> gives a fast sanity check before digging into app or network layers.&lt;/p>
&lt;h2 id="basic-commands">
 Basic commands
 &lt;a class="heading-link" href="#basic-commands">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Basic lookup:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">dig example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Get just the answer:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">dig +short example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="specific-record-types">
 Specific record types
 &lt;a class="heading-link" href="#specific-record-types">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>A records (IPv4):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">dig A example.com
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>AAAA records (IPv6):&lt;/p></description></item><item><title>Monitoring Kubernetes with watch and top</title><link>https://brtkwr.com/posts/2025-08-04-monitoring-kubernetes-with-watch-and-top/</link><pubDate>Mon, 04 Aug 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-04-monitoring-kubernetes-with-watch-and-top/</guid><description>&lt;p>I was watching a deployment roll out and kept running &lt;code>kubectl get pods&lt;/code> over and over like a nervous habit. Then I discovered &lt;code>watch&lt;/code> and felt slightly embarrassed. Combined with &lt;code>kubectl top&lt;/code>, these two commands handle most of my real-time cluster monitoring.&lt;/p>
&lt;h2 id="watch-for-live-updates">
 watch for live updates
 &lt;a class="heading-link" href="#watch-for-live-updates">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Basic watch:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">watch kubectl get pods
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Refreshes every 2 seconds by default. Faster refresh:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">watch -n 0.5 kubectl get pods
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Highlight differences:&lt;/p></description></item><item><title>Viewing Kubernetes pod logs</title><link>https://brtkwr.com/posts/2025-08-01-viewing-kubernetes-pod-logs/</link><pubDate>Fri, 01 Aug 2025 12:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-08-01-viewing-kubernetes-pod-logs/</guid><description>&lt;p>I spent 20 minutes staring at empty &lt;code>kubectl logs&lt;/code> output wondering why a crashing pod had no errors. The container was restarting before I could catch the logs. Then someone showed me the &lt;code>-p&lt;/code> flag - previous container logs. The actual stack trace had been there the whole time.&lt;/p>
&lt;h2 id="kubectl-logs">
 kubectl logs
 &lt;a class="heading-link" href="#kubectl-logs">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>View current logs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">kubectl logs my-pod-abc123
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>View logs from a crashed container (the previous instance):&lt;/p></description></item><item><title>Kubectl debugging workflow</title><link>https://brtkwr.com/posts/2025-07-29-kubectl-debugging-workflow/</link><pubDate>Tue, 29 Jul 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-29-kubectl-debugging-workflow/</guid><description>&lt;p>&lt;strong>TLDR&lt;/strong>: My order is &lt;code>describe&lt;/code> → &lt;code>logs -p&lt;/code> → &lt;code>events&lt;/code> → &lt;code>exec&lt;/code>. Most failures show up in events or previous logs.&lt;/p>
&lt;hr>
&lt;p>When a pod is in CrashLoopBackOff, plain &lt;code>kubectl logs&lt;/code> can miss the useful output. &lt;code>kubectl logs -p&lt;/code> is usually the first command that surfaces the failure.&lt;/p>
&lt;h2 id="my-debugging-workflow">
 My debugging workflow
 &lt;a class="heading-link" href="#my-debugging-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>When something&amp;rsquo;s broken, I check in this order:&lt;/p>
&lt;ol>
&lt;li>&lt;code>kubectl -n my-namespace describe pod/my-pod&lt;/code> - Shows events, restart count, and why it failed&lt;/li>
&lt;li>&lt;code>kubectl -n my-namespace logs my-pod -p&lt;/code> - Previous container logs (the &lt;code>-p&lt;/code> is crucial)&lt;/li>
&lt;li>&lt;code>kubectl -n my-namespace get events --sort-by='.lastTimestamp'&lt;/code> - Cluster-wide events&lt;/li>
&lt;li>&lt;code>kubectl -n my-namespace exec -it my-pod -- sh&lt;/code> - Only if I need to poke around inside&lt;/li>
&lt;/ol>
&lt;p>The most common issues I find: OOMKilled (check events), image pull failures (check events), config errors (check logs), and permission issues (check logs).&lt;/p></description></item><item><title>Rsync file sync patterns</title><link>https://brtkwr.com/posts/2025-07-26-rsync-file-sync-patterns/</link><pubDate>Sat, 26 Jul 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-26-rsync-file-sync-patterns/</guid><description>&lt;p>Rsync is reliable for repeat syncs, but small flag mistakes can be destructive. I use a dry-run-first workflow, especially with &lt;code>--delete&lt;/code>.&lt;/p>
&lt;h2 id="tldr-common-rsync-patterns">
 TLDR: Common rsync patterns
 &lt;a class="heading-link" href="#tldr-common-rsync-patterns">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Basic sync (local to remote)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avz ./ user@server:/path/to/dest/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Sync excluding common directories&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avz --exclude .venv --exclude node_modules --exclude .git ./ user@server:/dest/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Mirror (sync and delete removed files)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avz --delete ./ user@server:/dest/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Dry run (preview changes)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avzn ./ user@server:/dest/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Pull from remote to local&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avz user@server:/path/to/source/ ./local/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Copy between two remote servers (via local machine)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">rsync -avz user1@server1:/source/ user2@server2:/dest/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="basic-sync">
 Basic sync
 &lt;a class="heading-link" href="#basic-sync">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">rsync -avz ./ user@server:/path/to/dest/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The flags:&lt;/p></description></item><item><title>Managing Kubernetes deployments</title><link>https://brtkwr.com/posts/2025-07-25-managing-kubernetes-deployments/</link><pubDate>Fri, 25 Jul 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-25-managing-kubernetes-deployments/</guid><description>&lt;p>I once deployed a config change that crashed pods on startup. The rollout was halfway through before I noticed - half the replicas were healthy, half were in CrashLoopBackOff. &lt;code>kubectl rollout undo&lt;/code> had us back to the previous version in seconds. That&amp;rsquo;s when I learnt to always watch rollouts and know how to revert quickly.&lt;/p>
&lt;h2 id="my-deployment-workflow">
 My deployment workflow
 &lt;a class="heading-link" href="#my-deployment-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>At work, the recommended approach is GitOps (ArgoCD), so most changes go through git. But for quick debugging or emergencies:&lt;/p></description></item><item><title>Analysing Svelte build chunks</title><link>https://brtkwr.com/posts/2025-07-23-analysing-svelte-build-chunks/</link><pubDate>Wed, 23 Jul 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-23-analysing-svelte-build-chunks/</guid><description>&lt;p>I was working on a SvelteKit dashboard application and noticed the initial page load was sluggish. After investigating, I found we had over 80 separate chunk files - way too many. Too many small chunks means too many HTTP requests, which kills performance on slower connections.&lt;/p>
&lt;h2 id="what-was-too-big">
 What was too big
 &lt;a class="heading-link" href="#what-was-too-big">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The biggest culprits were:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Chart.js and its adapters&lt;/strong> (~150KB) being pulled into multiple routes&lt;/li>
&lt;li>&lt;strong>Lodash utilities&lt;/strong> scattered across components instead of tree-shaken imports&lt;/li>
&lt;li>&lt;strong>Icon sets&lt;/strong> loading all icons instead of just the ones we used&lt;/li>
&lt;/ul>
&lt;p>My target was to get below 20 chunks and keep the initial JS bundle under 200KB. For a dashboard app with lots of interactions, I think anything under 300KB total is reasonable.&lt;/p></description></item><item><title>Formatting PHP with php-cs-fixer</title><link>https://brtkwr.com/posts/2025-07-23-formatting-php-with-php-cs-fixer/</link><pubDate>Wed, 23 Jul 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-23-formatting-php-with-php-cs-fixer/</guid><description>&lt;p>I was working on a legacy PHP WooCommerce plugin that had inconsistent formatting - some files used tabs, others spaces, brace styles were all over the place. Rather than manually fixing thousands of lines, I used &lt;code>php-cs-fixer&lt;/code> to enforce consistent PHP coding standards across the entire codebase.&lt;/p>
&lt;p>Check for issues:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php-cs-fixer check .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Fix them:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php-cs-fixer fix .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can also run it on specific directories:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">php-cs-fixer fix Plugin/
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To check PHP syntax without fixing anything:&lt;/p></description></item><item><title>Setting up pre-commit hooks</title><link>https://brtkwr.com/posts/2025-07-23-setting-up-pre-commit-hooks/</link><pubDate>Wed, 23 Jul 2025 09:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-23-setting-up-pre-commit-hooks/</guid><description>&lt;p>I was tired of catching trivial issues in PR reviews - trailing whitespace, missing newlines, formatting inconsistencies. Pre-commit hooks solved this by running checks before code even gets committed.&lt;/p>
&lt;h2 id="installation">
 Installation
 &lt;a class="heading-link" href="#installation">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Install it:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">uv tool install pre-commit
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="my-actual-pre-commit-config">
 My actual pre-commit config
 &lt;a class="heading-link" href="#my-actual-pre-commit-config">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Here&amp;rsquo;s a solid starting configuration that balances thoroughness with speed:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">repos&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">repo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://github.com/pre-commit/pre-commit-hooks&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rev&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v4.5.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hooks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">trailing-whitespace&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">end-of-file-fixer&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">check-yaml&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--unsafe&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Allow custom YAML tags&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">check-added-large-files&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;--maxkb=500&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">check-merge-conflict&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">detect-private-key&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">repo&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://github.com/astral-sh/ruff-pre-commit&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rev&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v0.8.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">hooks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ruff&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>--&lt;span class="l">fix]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ruff-format&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># I don&amp;#39;t run mypy in pre-commit anymore - it&amp;#39;s too slow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># Better to run it in CI where you can cache dependencies&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Install the hooks:&lt;/p></description></item><item><title>Pytest tips: last failed and specific tests</title><link>https://brtkwr.com/posts/2025-07-21-pytest-tips-last-failed-and-specific-tests/</link><pubDate>Mon, 21 Jul 2025 16:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-pytest-tips-last-failed-and-specific-tests/</guid><description>&lt;p>I run pytest a lot. Here are the flags I use most.&lt;/p>
&lt;h2 id="my-development-workflow">
 My development workflow
 &lt;a class="heading-link" href="#my-development-workflow">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>When I&amp;rsquo;m writing code, I typically:&lt;/p>
&lt;ol>
&lt;li>Write the test first (or alongside the code)&lt;/li>
&lt;li>Run just that test: &lt;code>pytest tests/api/test_order_api.py::test_create_order -v&lt;/code>&lt;/li>
&lt;li>Fix failures, re-run with &lt;code>-lf&lt;/code> to only run failed tests&lt;/li>
&lt;li>Once passing, run the full suite to check for regressions&lt;/li>
&lt;/ol>
&lt;p>For debugging, I always use &lt;code>-s -v --tb=short&lt;/code> to see print output, verbose names, and shorter tracebacks.&lt;/p></description></item><item><title>GCloud workload identity for GitHub Actions</title><link>https://brtkwr.com/posts/2025-07-21-gcloud-workload-identity-for-github-actions/</link><pubDate>Mon, 21 Jul 2025 15:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-gcloud-workload-identity-for-github-actions/</guid><description>&lt;p>I was setting up GitHub Actions to deploy to GCP and needed to use workload identity instead of service account keys. Here&amp;rsquo;s how to manage the providers.&lt;/p>
&lt;h2 id="tldr">
 TLDR
 &lt;a class="heading-link" href="#tldr">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>Workload identity lets GitHub Actions authenticate to GCP without storing service account keys. It&amp;rsquo;s more secure because:&lt;/p>
&lt;ul>
&lt;li>No static credentials to rotate or leak&lt;/li>
&lt;li>Fine-grained control over which repos can access what&lt;/li>
&lt;li>Built-in audit trail of who authenticated when&lt;/li>
&lt;/ul>
&lt;p>The main gotcha: you need &lt;code>id-token: write&lt;/code> permission in your workflow or auth fails silently.&lt;/p></description></item><item><title>Ruff, the fast Python linter</title><link>https://brtkwr.com/posts/2025-07-21-ruff-the-fast-python-linter/</link><pubDate>Mon, 21 Jul 2025 14:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-ruff-the-fast-python-linter/</guid><description>&lt;p>I switched to Ruff because it&amp;rsquo;s ridiculously fast. On a ~50k line Python codebase, Flake8 + isort + pylint took about 45 seconds. Ruff does it in under 2 seconds. It&amp;rsquo;s written in Rust and it shows.&lt;/p>
&lt;h2 id="basic-usage">
 Basic usage
 &lt;a class="heading-link" href="#basic-usage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ruff check .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Fix issues automatically:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ruff check --fix .
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Run specific rules only:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ruff check --select RUF001 &lt;span class="c1"># unicode issues&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ruff check --select F403 &lt;span class="c1"># star imports&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I wanted to see what errors I had the most of:&lt;/p></description></item><item><title>Import existing resources into Terraform without downtime</title><link>https://brtkwr.com/posts/2025-07-21-import-existing-infrastructure-into-terraform/</link><pubDate>Mon, 21 Jul 2025 12:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-import-existing-infrastructure-into-terraform/</guid><description>&lt;p>I migrated manually created GCP workload identity pools and GitHub repositories into Terraform so they could be managed consistently in code. The main friction point was provider-specific import ID formats.&lt;/p>
&lt;h2 id="basic-import">
 Basic import
 &lt;a class="heading-link" href="#basic-import">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">terraform import module.my-app.github_repository.repo my-repo-name
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="import-blocks-terraform-15">
 Import blocks (Terraform 1.5+)
 &lt;a class="heading-link" href="#import-blocks-terraform-15">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>For team workflows, I prefer import blocks because they&amp;rsquo;re reviewable in code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="cl">&lt;span class="k">import&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> to&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">my_app&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">github_repository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">repo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n"> id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;my-repo-name&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Run:&lt;/p></description></item><item><title>Git worktrees for parallel development</title><link>https://brtkwr.com/posts/2025-07-21-git-worktrees-for-parallel-development/</link><pubDate>Mon, 21 Jul 2025 11:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-git-worktrees-for-parallel-development/</guid><description>&lt;p>I was constantly stashing changes to review PRs or fix urgent bugs on other branches. Stash, switch, fix, commit, switch back, pop stash, hope I didn&amp;rsquo;t mess something up. Git worktrees solved this entirely.&lt;/p>
&lt;h2 id="whats-a-worktree">
 What&amp;rsquo;s a worktree?
 &lt;a class="heading-link" href="#whats-a-worktree">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>A worktree is a separate working directory with a different branch checked out, all linked to the same repository. Create one like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">git worktree add ../fix-urgent-bug fix-urgent-bug
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you have two directories - your main one stays on whatever branch you were on, and the new one has &lt;code>fix-urgent-bug&lt;/code> checked out. No more stashing, no context switching, just &lt;code>cd&lt;/code> between them.&lt;/p></description></item><item><title>Modern CLI tools: fd and ripgrep</title><link>https://brtkwr.com/posts/2025-07-21-modern-cli-tools-fd-and-ripgrep/</link><pubDate>Mon, 21 Jul 2025 10:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-21-modern-cli-tools-fd-and-ripgrep/</guid><description>&lt;p>I stopped using &lt;code>find&lt;/code> and &lt;code>grep&lt;/code> ages ago. &lt;code>fd&lt;/code> and &lt;code>rg&lt;/code> (ripgrep) are faster and have better defaults. Switching to these was probably one of the quickest productivity wins I&amp;rsquo;ve had.&lt;/p>
&lt;h2 id="why-theyre-faster">
 Why they&amp;rsquo;re faster
 &lt;a class="heading-link" href="#why-theyre-faster">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>The main reason is &lt;code>.gitignore&lt;/code> support by default. If you&amp;rsquo;re searching a monorepo with thousands of files, skipping &lt;code>node_modules&lt;/code> and &lt;code>.venv&lt;/code> automatically makes a massive difference. On my work codebase, &lt;code>rg&lt;/code> is easily 5-10x faster than &lt;code>grep -r&lt;/code> for common searches.&lt;/p></description></item><item><title>Publishing Python packages to Artifact Registry with uv</title><link>https://brtkwr.com/posts/2025-07-20-publishing-python-packages-to-artifact-registry-with-uv/</link><pubDate>Sun, 20 Jul 2025 14:30:00 +0000</pubDate><guid>https://brtkwr.com/posts/2025-07-20-publishing-python-packages-to-artifact-registry-with-uv/</guid><description>&lt;p>I spent way too long trying to get &lt;code>uv publish&lt;/code> to work with Google Artifact Registry. The docs suggest using keyring, but that led me down a rabbit hole of authentication errors.&lt;/p>
&lt;h2 id="the-error">
 The error
 &lt;a class="heading-link" href="#the-error">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>My first attempt was the keyring approach from the uv docs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">uv tool install keyring --with keyrings.google-artifactregistry-auth
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">uv publish --index my-registry dist/*
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This gave me a 401:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">error: Failed to publish `my-package-0.1.0.tar.gz` to my-registry
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Caused by: Upload failed with status code 401 Unauthorized
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Even with &lt;code>gcloud auth application-default login&lt;/code> configured, it just wouldn&amp;rsquo;t pick up the credentials.&lt;/p></description></item><item><title>Trustless pseudo-random number generation</title><link>https://brtkwr.com/posts/2018-01-31-does-modulus-of-sum-of-a-uniform-distribution/</link><pubDate>Wed, 31 Jan 2018 00:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2018-01-31-does-modulus-of-sum-of-a-uniform-distribution/</guid><description>&lt;p>I have been working on an &lt;a href="https://github.com/strange-labs-uk/ethereum-lottery" class="external-link" target="_blank" rel="noopener">Ethereum lottery&lt;/a> Dapp with folks attending a meetup hosted by Strange Labs (Gloucester Road, Bristol - NOW CLOSED) and we faced a problem where we realised that Solidity does not have a built in random number generator and therefore picking a winner turned out to be more difficult that anyone imagined.&lt;/p>
&lt;p>In order to make the system truly trustless, one of the ideas involved each participant generating their own random number and submitting it to the contract when they buy their lottery tickets. However I was concerned that taking a sum of of these numbers would result in a normal distribution allowing the early and late participants to have a built in disadvantage.&lt;/p></description></item><item><title>Permission issues with xpra when tunnelling through SSH</title><link>https://brtkwr.com/posts/2017-03-15-permission-issues-with-xpra-on-ubuntu-16-04-when-tunnelling-through-ssh/</link><pubDate>Wed, 15 Mar 2017 19:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2017-03-15-permission-issues-with-xpra-on-ubuntu-16-04-when-tunnelling-through-ssh/</guid><description>&lt;p>I often log into a remote SSH terminal to use an &lt;code>ipython&lt;/code> shell. In doing so, it is necessary for me look at figures invoked by &lt;code>matplotlib&lt;/code> as I am exploring and analysing data. For this, I have comfortably been using a combination of &lt;code>screen&lt;/code> and &lt;code>xpra&lt;/code>. However, I recently hesitantly upgraded my Ubuntu distribution from 14.04 Trusty Tahr to 16.04 Xenial Xerus. And with all upgrades, I expected minor breakages. Little did I realise that it would break &lt;code>xpra&lt;/code> and interrupt my workflow sending me down a rabbit hole internet trawling for solutions. Turns out it is a known &lt;a href="https://bugs.launchpad.net/ubuntu/&amp;#43;source/xserver-xorg-video-dummy/&amp;#43;bug/1589447" class="external-link" target="_blank" rel="noopener">issue&lt;/a> on on launchpad but the discussion has not been active for a while and the provided solutions were incomprehensible to a lay user. It appears that simply updating &lt;code>xpra&lt;/code> to the latest version resolves the issue.&lt;/p></description></item><item><title>Enabling MathJax on a Jekyll server when offline</title><link>https://brtkwr.com/posts/2017-03-08-enabling-mathjax-on-a-jekyll-server-when-offline/</link><pubDate>Wed, 08 Mar 2017 19:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2017-03-08-enabling-mathjax-on-a-jekyll-server-when-offline/</guid><description>&lt;p>I like MathJax. It does a great job displaying equations on web pages written using $\rm\LaTeX$. However, I recently wrote a Jekyll &lt;a href="./posts/2015-03-10-how-well-does-population-distribution/" >blog post&lt;/a> which uses MathJax and tried to view it on a locally deployed server while commuting on a train journey with no Internet connection. Then, I realised that it did not render the equations at all because it relied on sending AJAX requests to a remote server. Something had to be done and this is how I went about fixing it.&lt;/p></description></item><item><title>File/text was not valid utf8 encoded</title><link>https://brtkwr.com/posts/2017-03-03-how-to-fix-texcount-file-text-was-not-valid-utf8-encoded-warning/</link><pubDate>Fri, 03 Mar 2017 19:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2017-03-03-how-to-fix-texcount-file-text-was-not-valid-utf8-encoded-warning/</guid><description>&lt;p>In the process of writing my thesis, I had to frequently check that I was not exceeding the word limit by running the following command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">texcount thesis.tex -inc -nosub
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And each time, I would get the following warning:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">!!! File/text was not valid utf8 encoded. !!!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>It was not until one of my supervisors noticed that something was causing strange artefacts to appear in the compiled PDF document in the feedback stage that I realised that it was to do with the Unicode characters hiding in the literature review chapter when I was copying block citation quotes from other publications. I was obviously not going to look through an entire chapter for Unicode characters so here is how I automated the process:&lt;/p></description></item><item><title>Getting Aimsun 8.1.3 to work on Ubuntu 16.04</title><link>https://brtkwr.com/posts/2016-09-05-getting-aimsun-8.1.3-to-work-on-ubuntu-16.04/</link><pubDate>Mon, 05 Sep 2016 19:54:00 +0000</pubDate><guid>https://brtkwr.com/posts/2016-09-05-getting-aimsun-8.1.3-to-work-on-ubuntu-16.04/</guid><description>&lt;p>After installing &lt;a href="https://www.aimsun.com" class="external-link" target="_blank" rel="noopener">TSS&amp;rsquo;s Aimsun&lt;/a> 8.1.3 on Ubuntu 16.04 (x64 build) on my machine, I tried to launch the program. However, the icon would appear briefly and close without any indication of what the problem was. The problem became obvious when starting up Aimsun through terminal using the following command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">/opt/Aimsun_8_3_0/Aimsun
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Instead of starting normally, I got the following message instead:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">Aimsun: error while loading shared libraries: libvpx.so.1: cannot open shared object file: No such file or directory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This was resolved by downloading version &lt;a href="https://launchpad.net/ubuntu/&amp;#43;archive/primary/&amp;#43;files/libvpx1_1.3.0-3ubuntu1_amd64.deb" class="external-link" target="_blank" rel="noopener">1.3.0-3&lt;/a> of &lt;a href="https://launchpad.net/ubuntu/%2Bsource/libvpx" class="external-link" target="_blank" rel="noopener">libvpx&lt;/a>. To install, run the following dpkg command on terminal:&lt;/p></description></item><item><title>Presentation for Geomob</title><link>https://brtkwr.com/posts/2016-01-21-presentation-for-geomob/</link><pubDate>Thu, 21 Jan 2016 20:00:00 +0000</pubDate><guid>https://brtkwr.com/posts/2016-01-21-presentation-for-geomob/</guid><description>&lt;p>My presentation for &lt;a href="http://geomobldn.org/post/133268214275/first-geomob-of-2016-21-jan-back-at-ucl" class="external-link" target="_blank" rel="noopener">Geomob in London&lt;/a>:&lt;/p>
&lt;iframe src="https://docs.google.com/presentation/d/1VMo2UxRva5U8SRCBIbwPR3gXF12P8zZIawTD7Yblkjs/embed?start=false&amp;loop=false&amp;delayms=3000" frameborder="0" width="750" height="450" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true">&lt;/iframe></description></item><item><title>Photography: Mundane, Surreal</title><link>https://brtkwr.com/posts/2015-12-23-mundane-surreal/</link><pubDate>Wed, 23 Dec 2015 16:34:00 +0000</pubDate><guid>https://brtkwr.com/posts/2015-12-23-mundane-surreal/</guid><description>&lt;p>Sometimes, the mundane assumes a surreal form and takes one by surprise. I had the pleasure of this encounter this morning and I felt the need to share it.&lt;/p>
&lt;p>&lt;img src="./images/mundane/mundane1.jpg" alt="bathroom">
&lt;img src="./images/mundane/mundane2.jpg" alt="toothbrush">
&lt;img src="./images/mundane/mundane3.jpg" alt="chair">
&lt;img src="./images/mundane/mundane4.jpg" alt="duvet">
&lt;img src="./images/mundane/mundane5.jpg" alt="towel">
&lt;img src="./images/mundane/mundane6.jpg" alt="box">
&lt;img src="./images/mundane/mundane7.jpg" alt="plates">
&lt;img src="./images/mundane/mundane8.jpg" alt="laptop">
&lt;img src="./images/mundane/mundane9.jpg" alt="bed">&lt;/p></description></item><item><title>What me eat by Macka B</title><link>https://brtkwr.com/posts/2015-12-18-what-me-eat-i-love-this-song/</link><pubDate>Fri, 18 Dec 2015 15:11:00 +0000</pubDate><guid>https://brtkwr.com/posts/2015-12-18-what-me-eat-i-love-this-song/</guid><description>&lt;p>I love this song. Vegan solidarity!&lt;/p>

 &lt;div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
 &lt;iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="allowfullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/FLqjLn0W5K0?autoplay=0&amp;amp;controls=1&amp;amp;end=0&amp;amp;loop=0&amp;amp;mute=0&amp;amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video">&lt;/iframe>
 &lt;/div>

&lt;p>Here is the lyrics borrowed from &lt;a href="http://genius.com/Macka-b-wha-me-eat-lyrics" class="external-link" target="_blank" rel="noopener">this site&lt;/a>:&lt;/p>
&lt;pre>&lt;code>[Intro]
Selamta (Greetings in Amharic)
Ital (natural) we Ital and Vegan we Vegan
I and I (we) eat from the earth and leave the animals to give birth
No deaders (dead flesh) No fur No feathers
When I tell people I don't eat meat, fish or dairy
They look at me strangely
They don't realise I eat a very wide variety
Listen to Macka.B
Yo!

[Hook x2]
Wha me eat them a wonder wha me eat
When me tell them say me nu (don't) eat no fish nor no meat no
Wha me eat them a wonder wha me nyam (eat)
When me tell them say that I'm a vegan

[Verse 1]
Well me nu eat no meat no fish no cheese nor no egg
Nothing with no foot no eye no wing nor no head
Nothing with no lip no ears no toe nor no leg
Prefer fruit and vegetables instead
Me careful and me choosy about what I'm eating
My medicines my food my food is my medicine
When I tell people that me nu eat dem deh (those) things
The look at me and scratch their chin
And start wondering

[Hook 2]
Wha me eat them a wonder wha me eat
When me tell them say me nu eat no fish nor no meat no
Wha me eat them a wonder wha me nyam
When me tell them say that I'm a vegan
Wha me eat them a wonder wha me eat
When me tell them say me nu eat no fish nor no meat no
Wha me eat them a wonder wha me eat
Dou you want to hear wha me eat?

[Verse 2]
I eat
Callaloo, Ackee, Sweet Potato
Yam, Banana and Tomato
Cabbage, Spinach, Avocado
Cho Cho, Butter Beans and Coco
Courgettes, Millet, Plantain
Rice and Peas and Pumpkin
Mango, Dates and Guava
Chick peas and Cassava
Brussel sprouts and Caulifower
Onion, Fennel and Cucumber
Plum, Pear and Papaya
Aubergine and Soya
Lime , Lentils and Quinoa
Wholemeal Bread and Wholemeal Flour
Watercress and Okra
Tofu and Sweet Pepper
Cous Cous and Carrots
Broccoli and Coconut
Peaches, Apples, Apricot
Breadfruit, Jackfruit, Sour sop
Pistachios, Cashews and Almonds
Walnuts, Peanuts also Pecan
Sesame Seeds, Sunflower..., Lemon
Orange, Pineapple and Melon
Bulghar Wheat and Garlic, Kiwi, Corn and Turnip
Pap Choy and Pomegranite, Hijiki and Rocket
Berries, Cherries and Strawberries
Beetroot, Grapefruit and Celeries
You see the meat's not necessary
We tell them say

[Hook x2]

[Verse 3]
Look how me big and me say look how me strong
Some people can't believe that me a vegetarian
If you want a healthy body check the real Rastaman
Cause Rastaman will tell you about the right nutrition
Me get my Calcium ,my Sodium me get Potassium
Me get my Zinc,me get my Iron and my Magnesium
Instead of nyam(eat) the fish I nyam what the fish nyam
Like the Kelp and Irish Moss that grow in the ocean
Me get me Proteins and my Minerals, me get me Calories
The Vitamins A the B the C the D the E,the F the G
Essential fatty acids like the Omega 3
Me get me fibre and me Carbohydrates in my body
Don't forget your Water drink a few glasses a day
The toxins in your body just flush them away
Some of the things you eat stop in your body and decay
When it comes to food I don't play
We tell them say

[Hook x2]

[Verse 4]
A lot of people would stop eat the meat
If they had to kill the animals before they could eat
Look at the way the animals them get treat
The unsanitary conditions where some of them keep
If we were supposed to eat the meat we would have sharp teeth
You wouldn't need a Knife and fork can you see it
You can't eat it raw you have to cook it complete
And put on Vegetable seasoning to make it taste sweet

[Hook x2]

[Verse 5]
It's up to you you can eat what you want to
You can be a vegetarian and be healthy too
There's a lot of choice around many foods are on view
I just remember some more I forgot to tell you
The Nectarines and tangerines and clementines and guanabana
Lychhe, oats and ginger, kale and spirulina
Mung beans, wholemeal pasta etc
&lt;/code>&lt;/pre></description></item><item><title>Use crontab to log CPU temperature on OSX!</title><link>https://brtkwr.com/posts/2015-12-17-log-temperature-using-cron-job/</link><pubDate>Thu, 17 Dec 2015 15:16:00 +0000</pubDate><guid>https://brtkwr.com/posts/2015-12-17-log-temperature-using-cron-job/</guid><description>&lt;p>I have just set up a way to automatically log temperature using OSX terminal because my computer was frequently overheating and I wanted to investigate why. Here is breakdown of how I did it.&lt;/p>
&lt;p>First of all, I installed &lt;a href="https://www.bresink.eu/Downloads/HardwareMonitor-CE.dmg" class="external-link" target="_blank" rel="noopener">Temperature Monitor for OSX&lt;/a> from this URL into the application folder.&lt;/p>
&lt;p>On terminal, I entered the following command to edit &lt;code>crontab&lt;/code> using &lt;code>vim&lt;/code> text editor.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">env &lt;span class="nv">EDITOR&lt;/span>&lt;span class="o">=&lt;/span>vim crontab -e
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the editor, I entered the following line which creates and appends date, time and temperatures from all your built-in sensors at a minute interval to &lt;code>~/Documents/temp.txt&lt;/code>. Feel free to change this to whatever you like.&lt;/p></description></item><item><title>Find and delete files and folders in OSX terminal</title><link>https://brtkwr.com/posts/2015-11-20-find-and-delete-files-and-folders-in-osx-terminal/</link><pubDate>Fri, 20 Nov 2015 10:54:00 +0000</pubDate><guid>https://brtkwr.com/posts/2015-11-20-find-and-delete-files-and-folders-in-osx-terminal/</guid><description>&lt;p>Finding and deleting files and folders in OSX is simple. Open your terminal. In order to just find your files/folders (&lt;strong>non destructive&lt;/strong>):&lt;/p>
&lt;p>For files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">find . -name &lt;span class="s2">&amp;#34;Icon?&amp;#34;&lt;/span> -type f
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For folders:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">find . -name &lt;span class="s2">&amp;#34;Icon?&amp;#34;&lt;/span> -type d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For files and folders:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">find . -name &lt;span class="s2">&amp;#34;Icon?&amp;#34;&lt;/span> -type f -o -name &lt;span class="s2">&amp;#34;Icon?&amp;#34;&lt;/span> -type d
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In order to delete them, just add &lt;code>-delete&lt;/code> operator to the end (&lt;strong>DESTRUCTIVE&lt;/strong>):&lt;/p>
&lt;p>For files:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">find . -name &lt;span class="s2">&amp;#34;Icon?&amp;#34;&lt;/span> -type f -delete
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For folders (although you may get a warning message saying &amp;lsquo;No such file or directory&amp;rsquo;):&lt;/p></description></item><item><title>Converting raster files from one reference system to another</title><link>https://brtkwr.com/posts/2015-10-19-converting-lots-of-raster-files-from-one-reference-system-to-another/</link><pubDate>Mon, 19 Oct 2015 13:07:14 +0000</pubDate><guid>https://brtkwr.com/posts/2015-10-19-converting-lots-of-raster-files-from-one-reference-system-to-another/</guid><description>&lt;p>I had to convert lots of OSGB EPSG:27700 raster files to LonLat EPSG:4326 format and here is how I did it on my OSX terminal. I also wanted to change the format from &lt;code>*.asc&lt;/code> to &lt;code>*.tif&lt;/code>. Supposing you already have &lt;code>gdal&lt;/code> libraries installed, just run the following code:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="nv">input_folder&lt;/span> &lt;span class="o">=&lt;/span> ???
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">output_folder&lt;/span>&lt;span class="o">=&lt;/span>../LonLat
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">cd&lt;/span> input_folder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir output_folder
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> file in *.asc&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> gdalwarp -s_srs &lt;span class="s2">&amp;#34;+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=375,-111,431,0,0,0,0 +units=m +no_defs&amp;#34;&lt;/span> -t_srs &lt;span class="s2">&amp;#34;+proj=longlat +datum=WGS84 +no_defs&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$file&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$folder&lt;/span>&lt;span class="s2">/`basename &lt;/span>&lt;span class="nv">$file&lt;/span>&lt;span class="s2"> .asc`.tif&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">done&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To utilise all CPUs, add &lt;code>-wo NUM_THREADS=ALL_CPUS&lt;/code> to the &lt;code>gdalwarp&lt;/code> options.&lt;/p></description></item><item><title>Fastest way to access a list that is a subset of an even bigger list</title><link>https://brtkwr.com/posts/2015-10-06-fastest-way-to-access-a-list-that-is-a-subset-of/</link><pubDate>Tue, 06 Oct 2015 10:13:14 +0100</pubDate><guid>https://brtkwr.com/posts/2015-10-06-fastest-way-to-access-a-list-that-is-a-subset-of/</guid><description>&lt;p>Say I have a list of objects called &lt;code>agents&lt;/code> with 100,000 objects.&lt;/p>
&lt;p>I have a second list called &lt;code>agents_per_step&lt;/code> which refers to 39,393 objects in &lt;code>agents&lt;/code>.&lt;/p>
&lt;p>Say I need to access the agents in the reference list. What is the fastest way to access these objects?&lt;/p>
&lt;p>I ran three tests:&lt;/p>
&lt;ol>
&lt;li>Loop through a list directly containing &lt;code>agent&lt;/code> objects&lt;/li>
&lt;li>Loop through a list of indices referring to &lt;code>agents&lt;/code> objects on a &lt;code>list&lt;/code>&lt;/li>
&lt;li>Loop through a list of indices referring to &lt;code>agents&lt;/code> objects on a &lt;code>dict&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>I present the results below:&lt;/p></description></item><item><title>Forum is Free: Housing and Energy</title><link>https://brtkwr.com/posts/2015-03-12-forum-is-free-housing-and-energy/</link><pubDate>Thu, 12 Mar 2015 16:21:11 +0000</pubDate><guid>https://brtkwr.com/posts/2015-03-12-forum-is-free-housing-and-energy/</guid><description>&lt;p>On 11 May, Bristol Green Party hosted a forum to discuss housing and energy at The Station in central Bristol.&lt;/p>
&lt;p>The meeting was chaired by Darren Hall, Green Party candidate for Bristol West and Director of Bristol Green Week and on the panel were:&lt;/p>
&lt;ol>
&lt;li>Oona Goldsworthy, Chief Executive of United Communities&lt;/li>
&lt;li>Mike Roberts, Director of HAB housing owned by Kevin McCloud&lt;/li>
&lt;li>Shankari Raj Edgar, Director of &lt;a href="http://nudgegroup.com" class="external-link" target="_blank" rel="noopener">Nudge Group&lt;/a>&lt;/li>
&lt;li>Keith Cowling, Chair of Bristol &lt;a href="http://bristolclt.org.uk" class="external-link" target="_blank" rel="noopener">Community Land Trust&lt;/a> and Director of Community Buildings Federation&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="./images/new_build.jpg" alt="image">&lt;/p></description></item><item><title>Bristol People’s Question Time</title><link>https://brtkwr.com/posts/2015-03-12-bristol-peoples-question-time/</link><pubDate>Thu, 12 Mar 2015 12:00:42 +0000</pubDate><guid>https://brtkwr.com/posts/2015-03-12-bristol-peoples-question-time/</guid><description>&lt;p>On 10th March, Bristol People’s Assembly hosted People’s Question Time at City Road Baptist Church in Stokes Croft.&lt;/p>
&lt;p>In the panel were Rufus Hound (Comedian and Actor), Pastor Eric Aidoo (from the Church), Romaine Phoenix (Chair of The People’s Assembly), John McInally (Vice President, PSC Union) and Sauda Kyalambuka (Educator and Board member of African Voices Forum).&lt;/p>
&lt;p>Hound opened with the remark that in this age of austerity, the rich got richer. He used the analogy of vampires living up in a high castle feeding on inhabitants of a village nearby idly standing by offering up members of their community as victims so that the rest can go on living. It would be unthinkable from our perspective to let that carry on happening and yet, the way that the corporations are leeching off our civilisation is exactly the same thing using austerity as an excuse. Therefore, he suggest that we ‘Kill the Vamps’!&lt;/p></description></item><item><title>Another Angry Voice</title><link>https://brtkwr.com/posts/2015-03-11-another-angry-voice/</link><pubDate>Wed, 11 Mar 2015 16:07:05 +0000</pubDate><guid>https://brtkwr.com/posts/2015-03-11-another-angry-voice/</guid><description>&lt;p>Another Angry Voice (AAV) aka Tom paid a visit to Bristol on 5th March.&lt;/p>
&lt;p>AAV introduced himself as a regular guy and emphasized that if he can do a political blog then anyone can.&lt;/p>
&lt;p>&lt;img src="./images/aav.png" alt="image">&lt;/p>
&lt;p>&lt;em>An example of an infographic by AAV.&lt;/em>&lt;/p>
&lt;p>AAV brings up the information war that left has been quite bad at communicating left wing ideas. Terms like &lt;code>Socialist&lt;/code>, &lt;code>Anarchist&lt;/code>, &lt;code>Communist&lt;/code>, &lt;code>Green&lt;/code> etc. are all loaded with distorted emotional meanings that people do not want to be associated with. He calls it &amp;lsquo;Tabloidesque` definition:&lt;/p></description></item><item><title>How well does population distribution predict evacuation time?</title><link>https://brtkwr.com/posts/2015-03-10-how-well-does-population-distribution/</link><pubDate>Tue, 10 Mar 2015 12:50:00 +0000</pubDate><guid>https://brtkwr.com/posts/2015-03-10-how-well-does-population-distribution/</guid><description>&lt;p>Previously, we tried to predict 90th percentile evacuation time $T{90}$ determined from ABM simulation using 90th percentile free flow clearance time $T90f$. Bigger the city, greater we can expect $T90f$ to be since it would take longer to traverse. Plotting $T90f$ vs $T90$ produces the figure below. $T90f$ under predicts $T90$ with a poor fit ($R=0.31$) but it is clear that $T90$ is never less than $T90f$!&lt;/p>
&lt;p>&lt;img src="./images/pop-dist/T90f-T90.png" alt="T90f vs T90">&lt;/p></description></item><item><title>First post!</title><link>https://brtkwr.com/posts/2015-03-04-first-post/</link><pubDate>Wed, 04 Mar 2015 14:29:13 +0000</pubDate><guid>https://brtkwr.com/posts/2015-03-04-first-post/</guid><description>&lt;p>I have just updated the website and finally acquired &lt;a href="https://brtkwr.com" class="external-link" target="_blank" rel="noopener">brtkwr.com&lt;/a> for good which is now hosted on Github pages too which is an excellent free of charge service if I may say so. It has just the right amount of manual tweaking to make you think you have control over how the information is presented on the website but sufficient structures already in place, for example, using the Jekyll static pages generator to automate most of the unpleasantries of reinventing the wheel of coding another site from scratch. This website will simply serve as a place where I can put my thoughts out there for people to look at if they want to.&lt;/p></description></item><item><title>About Bharat Kunwar</title><link>https://brtkwr.com/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://brtkwr.com/about/</guid><description>&lt;p>I&amp;rsquo;m Bharat (although I usually go by &lt;code>brtkwr&lt;/code> handle on the tinternet). I&amp;rsquo;ve lived in &lt;a href="https://www.openstreetmap.org/relation/5746665" class="external-link" target="_blank" rel="noopener">Bristol&lt;/a> since 2012 - a city I love love love!&lt;/p></description></item><item><title>CV</title><link>https://brtkwr.com/cv/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://brtkwr.com/cv/</guid><description/></item><item><title>Health Bridge — Privacy Policy</title><link>https://brtkwr.com/health-bridge/privacy/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://brtkwr.com/health-bridge/privacy/</guid><description>&lt;p>&lt;strong>Last updated: 23 March 2026&lt;/strong>&lt;/p>
&lt;p>Health Bridge is a free iOS app that syncs Apple Watch workouts to Garmin Connect. Your privacy matters — here&amp;rsquo;s how the app handles your data.&lt;/p>
&lt;h2 id="what-data-the-app-accesses">
 What data the app accesses
 &lt;a class="heading-link" href="#what-data-the-app-accesses">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>HealthKit workout data&lt;/strong>: heart rate, GPS routes, running dynamics, and other metrics recorded by your Apple Watch during workouts.&lt;/li>
&lt;li>&lt;strong>Garmin Connect credentials&lt;/strong>: your Garmin account login is used solely to authenticate with Garmin&amp;rsquo;s servers for uploading workout data.&lt;/li>
&lt;/ul>
&lt;h2 id="how-your-data-is-used">
 How your data is used
 &lt;a class="heading-link" href="#how-your-data-is-used">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Workout data is read from HealthKit, converted to FIT format, and uploaded directly to your Garmin Connect account.&lt;/li>
&lt;li>&lt;strong>All processing happens on your device.&lt;/strong> No workout data is sent to any server other than Garmin Connect.&lt;/li>
&lt;li>&lt;strong>No analytics, tracking, or telemetry&lt;/strong> is collected by this app.&lt;/li>
&lt;/ul>
&lt;h2 id="data-storage">
 Data storage
 &lt;a class="heading-link" href="#data-storage">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>Garmin authentication tokens are stored securely in your device&amp;rsquo;s Keychain.&lt;/li>
&lt;li>Sync records (which workouts have been synced) are stored locally on your device using SwiftData.&lt;/li>
&lt;li>&lt;strong>No data is stored on external servers&lt;/strong> controlled by the developer.&lt;/li>
&lt;/ul>
&lt;h2 id="third-party-services">
 Third-party services
 &lt;a class="heading-link" href="#third-party-services">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Garmin Connect&lt;/strong>: Workout data is uploaded to Garmin&amp;rsquo;s servers using their API. Garmin&amp;rsquo;s own privacy policy applies to data stored on their platform.&lt;/li>
&lt;li>&lt;strong>Apple HealthKit&lt;/strong>: The app reads data from HealthKit with your permission. Apple&amp;rsquo;s privacy policy governs HealthKit data.&lt;/li>
&lt;/ul>
&lt;h2 id="your-control">
 Your control
 &lt;a class="heading-link" href="#your-control">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;ul>
&lt;li>You can revoke HealthKit access at any time in Settings → Health → Health Bridge.&lt;/li>
&lt;li>You can log out of Garmin Connect within the app.&lt;/li>
&lt;li>Deleting the app removes all locally stored data, including sync records and Garmin tokens.&lt;/li>
&lt;/ul>
&lt;h2 id="contact">
 Contact
 &lt;a class="heading-link" href="#contact">
 &lt;i class="fa-solid fa-link" aria-hidden="true" title="Link to heading">&lt;/i>
 &lt;span class="sr-only">Link to heading&lt;/span>
 &lt;/a>
&lt;/h2>
&lt;p>If you have questions about this privacy policy, email &lt;a href="mailto:health-bridge-app@googlegroups.com" >health-bridge-app@googlegroups.com&lt;/a>.&lt;/p></description></item></channel></rss>