<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ntfy on hippotion</title><link>https://blog.hippotion.com/tags/ntfy/</link><description>Recent content in Ntfy on hippotion</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 29 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.hippotion.com/tags/ntfy/index.xml" rel="self" type="application/rss+xml"/><item><title>Every Robot in My House Can Text Me Now</title><link>https://blog.hippotion.com/posts/every-robot-texts-me/</link><pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate><guid>https://blog.hippotion.com/posts/every-robot-texts-me/</guid><description>My house is full of automation that never told me anything — until I gave it one push bus. The first thing I taught it to do was warn me before Claude Code cuts out mid-task.</description><content:encoded><![CDATA[<h2 id="the-silence">The silence</h2>
<p>My house runs on quiet little robots. A tracker watches my kombucha ferment. A
job narrates kids&rsquo; books in Hungarian. A media stack pulls and files things. Home
Assistant minds the sensors. A dozen services, all doing their jobs, all
completely mute. When a batch finished or an import failed, I found out the same
way every time: by going to look.</p>
<p>Then the silence got expensive. Claude Code stopped dead in the middle of a task
because I&rsquo;d burned through my plan&rsquo;s usage window — no warning, no countdown,
just a wall. The information <em>existed</em>; a dashboard in my own cluster was already
polling it. It just had no way to reach my pocket.</p>
<p>So I built one thing: a push bus. One place anything in the cluster can POST to,
that actually buzzes my phone. And the first job I gave it was to warn me before
my AI assistant goes dark.</p>
<hr>
<h2 id="the-boring-part-said-honestly">The boring part (said honestly)</h2>
<p>The bus is <a href="https://ntfy.sh">ntfy</a> — a self-hosted pub/sub notifier. Picking it
took about five minutes, because self-hosting ntfy for a homelab is a thoroughly
solved problem. There are at least three off-the-shelf bridges from Prometheus
Alertmanager to ntfy. I&rsquo;m not going to pretend the bus is the clever bit.</p>
<p>What I <em>did</em> do deliberately:</p>
<ul>
<li>📦 Deployed it <strong>GitOps-native</strong> — one entry in my app-of-apps, reconciled by
Argo CD, no <code>docker run</code> anywhere.</li>
<li>🔒 Locked it to <strong>deny-all auth</strong> with bearer tokens. Security alerts ride this
bus; a world-readable topic on a public URL was a non-starter. (Which also means
it sits <em>outside</em> my usual OAuth gate — the phone app can&rsquo;t do an interactive
login flow, so ntfy does its own token auth.)</li>
<li>🏷️ Topics by severity: <code>hl-crit</code>, <code>hl-warn</code>, <code>hl-info</code>, <code>hl-event</code>. Subscribe
and mute by how much I care.</li>
</ul>
<p>Then the interesting parts showed up at the edges, where they always do.</p>
<hr>
<h2 id="edge-one-my-own-firewall-403d-me">Edge one: my own firewall 403&rsquo;d me</h2>
<p>First test, the usage producer POSTing to <code>https://ntfy.hippotion.com</code>:</p>
<pre tabindex="0"><code>HTTP 403 Forbidden
error code: 1010
</code></pre><p>That <code>1010</code> looks like ntfy rejecting my token. It isn&rsquo;t. <strong>It&rsquo;s Cloudflare.</strong>
Error 1010 means &ldquo;your browser signature is banned&rdquo; — Cloudflare&rsquo;s bot protection
took one look at a Python script&rsquo;s <code>urllib</code> User-Agent and slammed the door.</p>
<p>My own producer couldn&rsquo;t reach my own bus, because the request left the cluster,
went all the way out to my own edge, and got flagged as a bot on the way back in.</p>
<p>The fix is the architecture I should&rsquo;ve had from the start: in-cluster producers
POST to the <strong>internal</strong> service address and never touch the public internet at
all.</p>
<pre tabindex="0"><code># wrong: out to Cloudflare and back, gets bot-blocked
https://ntfy.hippotion.com/hl-warn

# right: stays inside the cluster
http://ntfy.web-ntfy.svc.cluster.local/hl-warn
</code></pre><p>The phone still uses the public URL happily — the real ntfy app carries a
signature Cloudflare trusts. Only scripts trip 1010. <strong>Lesson: your own edge is
not your friend when you&rsquo;re a script. Keep cluster traffic in the cluster.</strong></p>
<hr>
<h2 id="edge-two-the-obvious-data-source-was-lying">Edge two: the obvious data source was lying</h2>
<p>To warn me about Claude usage, the naïve move is to parse Claude Code&rsquo;s local
logs — they sit right there in <code>~/.claude/projects/.../*.jsonl</code>, token counts and
all.</p>
<p>Don&rsquo;t. Those counts are <strong>unreliable for accounting</strong> — known to undercount,
wildly, in some cases by ~100x. Every tool that parses that JSONL inherits the
bug.</p>
<p>The number that&rsquo;s actually true lives in the claude.ai usage API — the same
<code>five_hour</code> and <code>seven_day</code> windows your plan enforces against. And I already had
a service polling exactly that. So the producer is just a tiny sidecar on that
existing pod, reading its <code>/api/usage</code> over <strong>localhost</strong> (same pod — no network
policy to negotiate, no second credential, nothing else hammering claude.ai):</p>
<ul>
<li>📈 ≥80% of a window → <code>hl-warn</code> (high).</li>
<li>🚨 ≥95% → <code>hl-crit</code> (urgent).</li>
<li>🔁 One ping per window per reset cycle, escalating warn→crit, keyed on the
reset timestamp so it never spams.</li>
</ul>
<p>The first time it mattered, my phone buzzed at 80% with hours of runway left
instead of a brick wall mid-task.</p>
<hr>
<h2 id="what-id-tell-past-me">What I&rsquo;d tell past me</h2>
<p>Three things, none of them about ntfy:</p>
<ol>
<li><strong>Reuse the signal you already have.</strong> I didn&rsquo;t build a usage poller — I bolted
a sidecar onto the one already running. The smallest producer is one that reads
localhost.</li>
<li><strong>Your own edge can betray you.</strong> A firewall that protects you from bots will
happily block your own automation. In-cluster talks in-cluster.</li>
<li><strong>Check whether your data source is telling the truth</strong> before you build an
alert on it. An alert you don&rsquo;t trust is worse than no alert — you&rsquo;ll learn to
ignore it, and then it&rsquo;ll be right once.</li>
</ol>
<p>Next, the high-leverage move: point Prometheus Alertmanager at the same bus, and
every infra alert I have — plus every one I&rsquo;ll ever add — lands on the phone
through one bridge. The kombucha ping can wait. The disk-full one can&rsquo;t.</p>
<p>The house is still full of quiet robots. The difference is now they know my
number.</p>
]]></content:encoded></item></channel></rss>