<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Technical Turtle</title>
    <description>The latest articles on DEV Community by Technical Turtle (@technical_turtle).</description>
    <link>https://dev.to/technical_turtle</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4041787%2Fc1e52b87-f6d4-47f7-9390-abc353300f33.png</url>
      <title>DEV Community: Technical Turtle</title>
      <link>https://dev.to/technical_turtle</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/technical_turtle"/>
    <language>en</language>
    <item>
      <title>The Postgres migration that locks your table at 3am, and how to catch it in review</title>
      <dc:creator>Technical Turtle</dc:creator>
      <pubDate>Thu, 23 Jul 2026 07:07:44 +0000</pubDate>
      <link>https://dev.to/technical_turtle/the-postgres-migration-that-locks-your-table-at-3am-and-how-to-catch-it-in-review-17pa</link>
      <guid>https://dev.to/technical_turtle/the-postgres-migration-that-locks-your-table-at-3am-and-how-to-catch-it-in-review-17pa</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- looks fine in review, locks your busiest table in prod:&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- same result, no outage:&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_email_nn&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_email_nn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The top one scans every row under an ACCESS EXCLUSIVE lock, so every read and write to the table blocks until it finishes. On a big table that is a four-minute outage. The bottom one gets to the same place without ever holding that lock during a scan. A generalist reviewing the diff cannot tell them apart, because both just say "make email NOT NULL".&lt;/p&gt;

&lt;p&gt;That is the whole problem. Whether a Postgres migration is safe has almost nothing to do with intent and everything to do with locks. A migration is dangerous when it holds a strong lock while it scans or rewrites a large table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; blocks reads and writes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SHARE&lt;/code&gt; blocks writes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a ten-row table you never notice. On a hot production table it is an outage. And there is a second trap: Flyway and Liquibase wrap each migration in a transaction by default, which changes what is safe and what errors out.&lt;/p&gt;

&lt;p&gt;Here are the changes most likely to hurt, and the safe rewrite for each.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. CREATE INDEX without CONCURRENTLY
&lt;/h3&gt;

&lt;p&gt;A plain &lt;code&gt;CREATE INDEX&lt;/code&gt; takes a &lt;code&gt;SHARE&lt;/code&gt; lock that blocks every write for the entire build. On a big table that is minutes of blocked writes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- danger&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- safe (note: cannot run inside a transaction)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; cannot run inside a transaction block, which is exactly how Flyway and Liquibase wrap migrations by default. A bare concurrent-index migration will error out. You have to disable the wrapping transaction for that one migration (Flyway: a script config flag; Liquibase: &lt;code&gt;runInTransaction="false"&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SET NOT NULL, directly
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ALTER TABLE ... ALTER COLUMN ... SET NOT NULL&lt;/code&gt; scans the whole table under &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; to prove no nulls exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- danger&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- safe (PG12+ skips the scan because a validated CHECK already proves it)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_email_nn&lt;/span&gt; &lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;VALID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;VALIDATE&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;users_email_nn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- scans, but only takes a SHARE UPDATE EXCLUSIVE lock (writes continue)&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;-- fast, no scan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. ADD CHECK without NOT VALID
&lt;/h3&gt;

&lt;p&gt;Same shape as above. A plain &lt;code&gt;ADD CONSTRAINT ... CHECK&lt;/code&gt; scans the whole table under &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt;. Add it &lt;code&gt;NOT VALID&lt;/code&gt; first (instant, only checks new rows), then &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; in a separate step (scans under a weaker lock that lets writes continue).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. ADD FOREIGN KEY without NOT VALID
&lt;/h3&gt;

&lt;p&gt;Adding a foreign key validates every existing row while holding a write-blocking lock on both the referencing and referenced tables. Same fix: add it &lt;code&gt;NOT VALID&lt;/code&gt;, then &lt;code&gt;VALIDATE CONSTRAINT&lt;/code&gt; separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. ADD COLUMN with a volatile DEFAULT or SERIAL
&lt;/h3&gt;

&lt;p&gt;Since PG11, adding a column with a &lt;em&gt;constant&lt;/em&gt; default is cheap (metadata only). But a &lt;em&gt;volatile&lt;/em&gt; default has to be evaluated per row, which rewrites the whole table under &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- danger: gen_random_uuid(), random(), and SERIAL are volatile -&amp;gt; full rewrite&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;-- safe&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;-- nullable, cheap&lt;/span&gt;
&lt;span class="c1"&gt;-- backfill in batches (see 8), then:&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A constant default like &lt;code&gt;now()&lt;/code&gt; evaluated once, or a literal, is fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. ALTER COLUMN TYPE
&lt;/h3&gt;

&lt;p&gt;Changing a column type rewrites the table, with a few binary-coercible exceptions (for example &lt;code&gt;varchar&lt;/code&gt; to &lt;code&gt;text&lt;/code&gt;, or widening a &lt;code&gt;varchar(n)&lt;/code&gt;). For anything else: add a new column, backfill it, swap in application code, drop the old column in a later deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. ADD PRIMARY KEY or UNIQUE, directly
&lt;/h3&gt;

&lt;p&gt;Adding these builds the underlying index under &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt;. Build the index first, concurrently, then attach it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;CONCURRENTLY&lt;/span&gt; &lt;span class="n"&gt;orders_pkey_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;orders_pkey&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;orders_pkey_idx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. Backfills in the wrong order, or one giant statement
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;UPDATE big_table SET ...&lt;/code&gt; with no WHERE bound takes one long transaction, bloats the table, and can deadlock. And backfilling &lt;em&gt;after&lt;/em&gt; you have already added the gating constraint fails. The order that works: add the column nullable, batch-backfill (by primary key ranges, committing each batch), validate, then tighten the constraint, across separate deploys.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Lock-taking DDL with no lock_timeout
&lt;/h3&gt;

&lt;p&gt;Even a fast &lt;code&gt;ALTER&lt;/code&gt; queues behind long-running queries, and everything queues behind &lt;em&gt;it&lt;/em&gt;. One slow ALTER can stall all traffic to a table. Bound the wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="p"&gt;...;&lt;/span&gt;   &lt;span class="c1"&gt;-- if it cannot get the lock in 2s, it fails instead of stalling the app; retry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  10. Destructive operations
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;DROP COLUMN&lt;/code&gt;, &lt;code&gt;DROP SCHEMA CASCADE&lt;/code&gt; are irreversible and break rolling deploys where old app instances still reference the object. Stop referencing it in the app, deploy, then drop it in a later migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Almost every safe rewrite above is the same move: never hold a strong lock while you scan or rewrite. Split the dangerous one-step change into a fast metadata change plus a separate, weakly-locked scan or a batched backfill. Do it across deploys.&lt;/p&gt;

&lt;h2&gt;
  
  
  I turned this into a tool
&lt;/h2&gt;

&lt;p&gt;I do this review by hand often enough that I encoded it. The &lt;a href="https://technical-turtle.com/postgres-migration-auditor" rel="noopener noreferrer"&gt;Postgres Migration Safety Auditor&lt;/a&gt; reads a Flyway or Liquibase migration (SQL, XML, YAML, JSON), parses it with libpg_query (the same parser Postgres uses, so a column named &lt;code&gt;type&lt;/code&gt; or a dollar-quoted body is understood, not misread by a regex), and flags each hazard with the reason and, where it can justify one from a cited catalog, the safe rewrite. 36 rules, every one pointing at an official Postgres, Flyway, or Liquibase source.&lt;/p&gt;

&lt;p&gt;It runs three ways: as a CLI, as a CI gate (machine-readable JSON, exit codes keyed to severity so it fails the build on a blocker), or as a Claude Code skill. It never connects to your database and never runs the migration, so it needs no credentials and nothing leaves your machine. Python 3, nothing to &lt;code&gt;pip install&lt;/code&gt; (the parser is bundled per platform). One-time EUR 14.99.&lt;/p&gt;

&lt;p&gt;The full ten-hazard cheatsheet is free on the landing page, so grab that even if the tool is not for you: &lt;a href="https://technical-turtle.com/postgres-migration-auditor#cheatsheet" rel="noopener noreferrer"&gt;https://technical-turtle.com/postgres-migration-auditor#cheatsheet&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also a free, open-source version, &lt;a href="https://github.com/technical-turtle/pg-migration-guard" rel="noopener noreferrer"&gt;pg-migration-guard&lt;/a&gt;, that runs these ten rules as a CLI and a GitHub Action, if you want the check in CI without the full catalog.&lt;/p&gt;

&lt;p&gt;Most migration outages are one of these ten. Knowing them is most of the fight; automating the check across all 36 rules is the rest.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>Catch the dangerous Postgres migration at the CI step</title>
      <dc:creator>Technical Turtle</dc:creator>
      <pubDate>Wed, 22 Jul 2026 15:36:52 +0000</pubDate>
      <link>https://dev.to/technical_turtle/catch-the-dangerous-postgres-migration-at-the-ci-step-1dd9</link>
      <guid>https://dev.to/technical_turtle/catch-the-dangerous-postgres-migration-at-the-ci-step-1dd9</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- looks routine in review, locks your table in production:&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_orders_customer&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both of those statements hold a lock that blocks traffic while they scan or build against the table. On a small table you never notice. On a busy production table it is downtime, and a diff review does not catch it because the SQL looks fine.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/pg-migration-guard" rel="noopener noreferrer"&gt;pg-migration-guard&lt;/a&gt; is a free tool that reads the migration and flags these before they reach production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pg-migration-guard V42__orders.sql

  WARN  IDX01  CREATE INDEX without CONCURRENTLY   [becomes a blocker on a large or busy table]
    Why:  Plain CREATE INDEX takes a lock that blocks all writes until the build finishes.
    Safe: CREATE INDEX CONCURRENTLY idx ON tbl (col);  -- outside a transaction

  WARN  NN01  SET NOT NULL scans the whole table under ACCESS EXCLUSIVE
    Safe: ADD CONSTRAINT c CHECK (col IS NOT NULL) NOT VALID; VALIDATE CONSTRAINT c; then SET NOT NULL;

Summary: 0 blocker, 2 warn, 0 advisory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why migrations bite
&lt;/h2&gt;

&lt;p&gt;Whether a Postgres migration is safe has almost nothing to do with intent and everything to do with locks. A migration is dangerous when it holds a strong lock while it scans or rewrites a large table. &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; blocks reads and writes; &lt;code&gt;SHARE&lt;/code&gt; blocks writes. The change that reads fine in review is the one that quietly takes that lock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it in CI
&lt;/h2&gt;

&lt;p&gt;The most useful place for this is the pull request, so a risky migration fails the build before anyone merges it. There is a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;migration-guard&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db/migration/**.sql"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;guard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.x"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;technical-turtle/pg-migration-guard@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db/migration&lt;/span&gt;
          &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blocker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each finding shows up as an inline annotation on the diff, so the author sees exactly which line is the problem and what the safe rewrite is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Or run it locally
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pg-migration-guard
pg-migration-guard db/migration/V42__add_index.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human output by default, &lt;code&gt;--format json&lt;/code&gt; for tooling, &lt;code&gt;--fail-on {blocker,warn,advisory,none}&lt;/code&gt; for the exit code. Python 3.8+, no other dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it catches
&lt;/h2&gt;

&lt;p&gt;Ten of the most common migration foot-guns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CREATE INDEX&lt;/code&gt; without &lt;code&gt;CONCURRENTLY&lt;/code&gt;, and a &lt;code&gt;CONCURRENTLY&lt;/code&gt; index op inside a transaction (which errors out)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SET NOT NULL&lt;/code&gt; directly (scans the whole table under a strong lock)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADD CHECK&lt;/code&gt; and &lt;code&gt;ADD FOREIGN KEY&lt;/code&gt; without &lt;code&gt;NOT VALID&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADD COLUMN&lt;/code&gt; with a volatile &lt;code&gt;DEFAULT&lt;/code&gt;, and &lt;code&gt;ALTER COLUMN TYPE&lt;/code&gt; (both rewrite the table)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADD PRIMARY KEY&lt;/code&gt; / &lt;code&gt;UNIQUE&lt;/code&gt; directly&lt;/li&gt;
&lt;li&gt;backfill ordering mistakes and unbatched &lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;lock-taking DDL with no &lt;code&gt;lock_timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destructive ops (&lt;code&gt;DROP TABLE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt;, &lt;code&gt;DROP COLUMN&lt;/code&gt;, &lt;code&gt;DROP SCHEMA&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each finding names the hazard, links to the official Postgres docs, and prints the safe rewrite.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;It parses the SQL with libpg_query, the same parser PostgreSQL itself uses, so a column named like a keyword, a schema-qualified table, or a dollar-quoted body is read correctly rather than misread by a regex. The parser is bundled as a small BSD-licensed binary and bound through the Python standard library, so there are no third-party dependencies and the tool never connects to a database or the network. It is MIT-licensed: &lt;a href="https://github.com/technical-turtle/pg-migration-guard" rel="noopener noreferrer"&gt;https://github.com/technical-turtle/pg-migration-guard&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Full coverage
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pypi.org/project/pg-migration-guard" rel="noopener noreferrer"&gt;pg-migration-guard&lt;/a&gt; is the free version of the Postgres Migration Safety Auditor. The paid tool checks 36 rules (not 12), each linked to an official source, and adds Liquibase XML / YAML / JSON changelog support, Postgres-version and table-size aware checks, a config file with per-rule mute controls, and a Claude Code skill: &lt;a href="https://technical-turtle.com/postgres-migration-auditor" rel="noopener noreferrer"&gt;https://technical-turtle.com/postgres-migration-auditor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run it against your migrations directory once, before your next deploy. The old migrations nobody has looked at since are usually where it finds something.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
      <category>sql</category>
    </item>
    <item>
      <title>Claude Code hides your context % and usage limits. Here's how to see them.</title>
      <dc:creator>Technical Turtle</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:22:02 +0000</pubDate>
      <link>https://dev.to/technical_turtle/claude-code-hides-your-context-and-usage-limits-heres-how-to-see-them-37he</link>
      <guid>https://dev.to/technical_turtle/claude-code-hides-your-context-and-usage-limits-heres-how-to-see-them-37he</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;before:  ~/code/myapp  main

after:   ctx 48% | 5h 14% ~1h20m | 7d 21% ~5d10h | opus-4.8   ~/code/myapp  main
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command, and your Claude Code statusline now shows how full your context is and how close you are to the Pro/Max limits, each with a reset countdown, in front of whatever bar you already run.&lt;/p&gt;

&lt;p&gt;Claude Code hides both of those numbers until you slam into them: the context compacts out from under you, or you hit a usage limit mid-task, with no warning. And if you write hooks or plugins, you cannot read those numbers at all. A &lt;code&gt;PreToolUse&lt;/code&gt; hook that wants to checkpoint before the context fills has no way to ask "how full are we". The data is not in the hook payload. Here is the fix, and the one place Claude Code will actually tell you these numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the data actually lives
&lt;/h2&gt;

&lt;p&gt;There is exactly one place Claude Code exposes this. Not the hooks API, not a plugin call, not an environment variable: the &lt;code&gt;statusLine&lt;/code&gt; command. Every render, Claude Code pipes a JSON payload to your configured statusline program, and that payload contains the authoritative fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"used_percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;47.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"context_window_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rate_limits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"five_hour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"used_percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resets_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1783359600&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seven_day"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"used_percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"resets_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1783368000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-1"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;context_window.used_percentage&lt;/code&gt; is the real context fullness. &lt;code&gt;rate_limits&lt;/code&gt; (present on Pro/Max OAuth) is your 5h and 7d usage, each with a Unix-epoch &lt;code&gt;resets_at&lt;/code&gt;. This is the only place Claude Code shows them. So if you want the numbers, the statusline is where you have to stand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/cc-context-telemetry" rel="noopener noreferrer"&gt;cc-context-telemetry&lt;/a&gt; is a small statusLine wrapper that does two things with that payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One, it puts the numbers on your bar&lt;/strong&gt; (the after line at the top). It prepends context used %, the 5h and 7d limits each with a reset countdown, and the current model to whatever your own statusline already prints. It wraps ANY statusline command, not a specific one, so you keep your existing bar and get the segment in front of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two, it writes that telemetry to a per-session file your hooks can finally read.&lt;/strong&gt; This is the part that is not obvious. Because the wrapper is standing in the one place Claude Code shows the data, it can persist it for everything else. Your hooks read it back with a one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; cc-context-telemetry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set it as your &lt;code&gt;statusLine&lt;/code&gt; in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cc-context-telemetry-statusline"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a new session (settings changes take effect in a fresh one) and your bar is now &lt;code&gt;ctx % | 5h % | 7d % | model&lt;/code&gt; with reset countdowns.&lt;/p&gt;

&lt;p&gt;Already run a statusline you want to keep? Point the wrapper at it and the segment prepends on the same line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cc-context-telemetry-statusline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"CCT_WRAP"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your existing statusline command&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pick which segments show, and in what order, with &lt;code&gt;CCT_SEGMENTS&lt;/code&gt; (default &lt;code&gt;ctx,5h,7d,model&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;CCT_SEGMENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ctx,5h,7d"&lt;/span&gt;          &lt;span class="c"&gt;# drop the model&lt;/span&gt;
&lt;span class="nv"&gt;CCT_SEGMENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5h,7d"&lt;/span&gt;              &lt;span class="c"&gt;# just the usage limits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  For hook and plugin authors
&lt;/h2&gt;

&lt;p&gt;This is the reason the project exists. In any hook, read the latest reading and act on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readTelemetry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cc-context-telemetry&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// hook stdin&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readTelemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fresh&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextPct&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// near the wall: checkpoint, summarize, or pause&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;readTelemetry(sessionId)&lt;/code&gt; returns the normalized reading (&lt;code&gt;contextPct&lt;/code&gt;, &lt;code&gt;fiveHourPct&lt;/code&gt;, &lt;code&gt;sevenDayPct&lt;/code&gt;, &lt;code&gt;fiveHourResetsAt&lt;/code&gt;, &lt;code&gt;sevenDayResetsAt&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, and a &lt;code&gt;fresh&lt;/code&gt; flag that rejects stale or non-finite values), or &lt;code&gt;null&lt;/code&gt; if there is nothing yet. Now a plugin can throttle itself as context fills, warn before a usage window resets, or checkpoint long work before a compaction wipes the session. All of it was impossible without a bridge out of the statusline.&lt;/p&gt;

&lt;h2&gt;
  
  
  It has to be safe to run every render
&lt;/h2&gt;

&lt;p&gt;Claude Code runs your statusLine on every render and kills that process each time to keep it bounded. That is a hostile environment for anything heavy. Two design choices keep it clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The per-render path is &lt;strong&gt;pure POSIX shell, with no Node on the hot path&lt;/strong&gt;. It reads the payload, writes the raw file atomically, prints the segment. All JSON parsing happens later, on demand, only when a hook actually calls &lt;code&gt;readTelemetry&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you wrap an existing bar, the entry &lt;code&gt;exec&lt;/code&gt;s your command, so it BECOMES your statusline process rather than spawning a child. What Claude Code kills each render is the whole thing. Nothing outlives a render to orphan or pile up. (An earlier design that spawned Node every render did pile up under the render-kills; the exec-through wrapper does not.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Node 18+ is needed only for the hooks API, never for the bar itself. It is cross-platform (CI-tested on Linux, macOS, and Windows via Git Bash), never throws, and never calls &lt;code&gt;claude&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accurate across your open sessions
&lt;/h2&gt;

&lt;p&gt;The 5h and 7d numbers are account-wide, and Claude Code only refreshes them when a session makes an API call. So cc-context-telemetry shows the freshest reading across all your open sessions on the machine ("latest API call wins"): your sessions agree on one number instead of each showing its own stale one. Context % is per session and always current. (The reconciliation is per machine, so separate machines each track their own.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;Free and MIT-licensed, zero third-party dependencies.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: &lt;a href="https://www.npmjs.com/package/cc-context-telemetry" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/cc-context-telemetry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/technical-turtle/cc-context-telemetry" rel="noopener noreferrer"&gt;https://github.com/technical-turtle/cc-context-telemetry&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is one of the developer tools I build under Technical Turtle. If you also ship database changes and want to catch the migration that locks your production table before it does, the &lt;a href="https://technical-turtle.com/postgres-migration-auditor" rel="noopener noreferrer"&gt;Postgres Migration Safety Auditor&lt;/a&gt; is the paid one.&lt;/p&gt;

&lt;p&gt;If you build something with this telemetry in a hook, I would love to see it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>devtools</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
