<?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: Amayo Clinton</title>
    <description>The latest articles on DEV Community by Amayo Clinton (@amayo_clinton).</description>
    <link>https://dev.to/amayo_clinton</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%2F3892796%2F34cc10e4-0ae9-44bd-901e-772b0144c161.jpg</url>
      <title>DEV Community: Amayo Clinton</title>
      <link>https://dev.to/amayo_clinton</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amayo_clinton"/>
    <language>en</language>
    <item>
      <title>[Boost]</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Mon, 20 Jul 2026 15:45:41 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/-87d</link>
      <guid>https://dev.to/amayo_clinton/-87d</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/emmabostian/101-tips-for-being-a-great-programmer-human-36nl" class="crayons-story__hidden-navigation-link"&gt;101 Tips For Being A Great Programmer (&amp;amp; Human)&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/emmabostian" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F123155%2Fcac9093b-f5a4-49e8-92c8-13c44121115a.jpg" alt="emmabostian profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/emmabostian" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Emma Bostian ✨
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Emma Bostian ✨
                
              
              &lt;div id="story-author-preview-content-134054" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/emmabostian" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F123155%2Fcac9093b-f5a4-49e8-92c8-13c44121115a.jpg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Emma Bostian ✨&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/emmabostian/101-tips-for-being-a-great-programmer-human-36nl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 9 '19&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/emmabostian/101-tips-for-being-a-great-programmer-human-36nl" id="article-link-134054"&gt;
          101 Tips For Being A Great Programmer (&amp;amp; Human)
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/career"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;career&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/emmabostian/101-tips-for-being-a-great-programmer-human-36nl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3366&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/emmabostian/101-tips-for-being-a-great-programmer-human-36nl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              202&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            13 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Nostr, Explained for Developers — A Deep Dive</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:41:16 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/nostr-explained-for-developers-a-deep-dive-204n</link>
      <guid>https://dev.to/amayo_clinton/nostr-explained-for-developers-a-deep-dive-204n</guid>
      <description>&lt;p&gt;If you've spent any time in the Bitcoin or decentralized-social corners of the internet&lt;br&gt;
lately, you've probably seen the word &lt;strong&gt;Nostr&lt;/strong&gt; thrown around next to terms like "zaps,"&lt;br&gt;
"relays," "npub," and "NIP-whatever." Most explainers stop at the surface — "it's&lt;br&gt;
decentralized Twitter." That's true, but it undersells what's actually going on and&lt;br&gt;
skips the parts that matter if you're a developer deciding whether to build on it.&lt;/p&gt;

&lt;p&gt;This post goes deeper: the protocol's actual wire format, why it was designed the way it&lt;br&gt;
was, how it compares to the alternatives that came before it, what real production&lt;br&gt;
systems look like on top of it, and where the sharp edges are. By the end you should be&lt;br&gt;
able to read the spec yourself and know exactly where to start building.&lt;/p&gt;
&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What Nostr actually is and isn't&lt;/li&gt;
&lt;li&gt;The problem it was designed to solve&lt;/li&gt;
&lt;li&gt;The protocol, piece by piece: keys, events, relays, clients&lt;/li&gt;
&lt;li&gt;How the wire protocol actually works (REQ / EVENT / CLOSE)&lt;/li&gt;
&lt;li&gt;NIPs: the extension system that lets the protocol grow&lt;/li&gt;
&lt;li&gt;Zaps: the feature that makes Nostr different from everything before it&lt;/li&gt;
&lt;li&gt;Nostr vs. ActivityPub vs. Bluesky's AT Protocol&lt;/li&gt;
&lt;li&gt;What real production apps look like on this stack&lt;/li&gt;
&lt;li&gt;The honest tradeoffs and open problems&lt;/li&gt;
&lt;li&gt;Getting started: a minimal working example&lt;/li&gt;
&lt;li&gt;Where this is heading&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  1. What Nostr actually is and isn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Nostr&lt;/strong&gt; stands for &lt;strong&gt;N&lt;/strong&gt;otes and &lt;strong&gt;O&lt;/strong&gt;ther &lt;strong&gt;S&lt;/strong&gt;tuff &lt;strong&gt;T&lt;/strong&gt;ransmitted by &lt;strong&gt;R&lt;/strong&gt;elays. It&lt;br&gt;
was first sketched out by a pseudonymous developer known as fiatjaf in late 2020, with the&lt;br&gt;
explicit goal of being "the simplest possible thing that works" — a deliberate reaction&lt;br&gt;
to how complex and heavyweight ActivityPub (the protocol behind Mastodon) had become.&lt;/p&gt;

&lt;p&gt;What it is: an open protocol specification — not a company, not a single app, not a&lt;br&gt;
blockchain, not a token. There's no Nostr Inc. to shut down and no central server to&lt;br&gt;
subpoena, because there isn't one. It's closer in spirit to email (SMTP) or the web&lt;br&gt;
(HTTP) than to a product — a shared format that many independent implementations agree&lt;br&gt;
to speak.&lt;/p&gt;

&lt;p&gt;What it isn't: a blockchain. This trips people up constantly because of how tightly it's&lt;br&gt;
associated with Bitcoin. There's no consensus mechanism, no mining, no global ledger of&lt;br&gt;
truth. Events aren't "mined" — they're just signed and broadcast. The only place Bitcoin&lt;br&gt;
actually enters the picture is optional: the Lightning Network is used for in-protocol&lt;br&gt;
payments (zaps), and secp256k1 — the same elliptic curve Bitcoin uses for keys — happens&lt;br&gt;
to be the curve Nostr identities are built on, mostly because it's fast, well-audited,&lt;br&gt;
and developers building in this space already had tooling for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. The problem it was designed to solve
&lt;/h2&gt;

&lt;p&gt;Every centralized social platform, no matter how well-intentioned, has the same structural&lt;br&gt;
weakness: your identity, your social graph, and your content all live inside one&lt;br&gt;
company's database, under one company's rules. If that company suspends you, changes its&lt;br&gt;
algorithm, sells to a new owner, or shuts down entirely, everything you built there is&lt;br&gt;
gone — not because you did anything wrong, but because you never actually owned any of&lt;br&gt;
it. You were a tenant.&lt;/p&gt;

&lt;p&gt;Mastodon and the wider Fediverse tried to fix this with federation — instead of one&lt;br&gt;
company, thousands of independently run servers, each running the ActivityPub protocol,&lt;br&gt;
talking to each other. This is a real improvement, but it only partially solves the&lt;br&gt;
problem: your identity is still tied to whichever server you signed up on&lt;br&gt;
(&lt;code&gt;@you@mastodon.social&lt;/code&gt;). If that specific server's admin bans you, or the server shuts&lt;br&gt;
down, you lose your handle, your followers, and your post history, even though the wider&lt;br&gt;
network survives. You've traded one company for one admin.&lt;/p&gt;

&lt;p&gt;Nostr's answer is to go one layer deeper: separate &lt;strong&gt;identity&lt;/strong&gt; from &lt;strong&gt;hosting&lt;/strong&gt;&lt;br&gt;
entirely, so that neither a company nor a single server admin can hold your identity&lt;br&gt;
hostage.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. The protocol, piece by piece
&lt;/h2&gt;
&lt;h3&gt;
  
  
  3.1 Identity is a keypair, not an account
&lt;/h3&gt;

&lt;p&gt;There is no sign-up flow on Nostr in the traditional sense. "Creating an account" means&lt;br&gt;
generating a standard secp256k1 keypair, client-side, with no server involved at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;nsec&lt;/strong&gt; (private key) — the thing that lets you sign events. Never uploaded anywhere
in plaintext, never sent to a server, ever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npub&lt;/strong&gt; (public key) — your identity, in a human-shareable, Bech32-encoded format.
This &lt;em&gt;is&lt;/em&gt; you, across every Nostr app that exists, forever, unless you choose to
generate a new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because no company or server issues this identity, none of them can revoke it. If you&lt;br&gt;
stop trusting one client, you install a different one, log in with the same key, and your&lt;br&gt;
followers, your post history, and your profile are all instantly there — not because&lt;br&gt;
they were "migrated," but because they were never tied to that app in the first place.&lt;br&gt;
This is structurally impossible to replicate on Twitter, Instagram, or Discord, where&lt;br&gt;
your identity is a row in a database you don't have access to.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.2 Content is a signed event — and that's the &lt;em&gt;entire&lt;/em&gt; data model
&lt;/h3&gt;

&lt;p&gt;This is the part that genuinely surprises people the first time they read the spec:&lt;br&gt;
there is exactly one data structure in Nostr. A post, a profile update, a like, a direct&lt;br&gt;
message, a long-form article, a Lightning payment receipt — all of them are the same&lt;br&gt;
JSON shape:&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;"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;"5c83da777af..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pubkey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"91cf9...f857"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_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;1721390400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&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="s2"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"some-other-pubkey-being-replied-to"&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="s2"&gt;"e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id-of-the-note-being-replied-to"&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;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello, nostr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"908a15e4f8de..."&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;Breaking that down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;id&lt;/code&gt;&lt;/strong&gt; — a SHA-256 hash of the serialized event data. This means the ID isn't
assigned by a server; it's a deterministic fingerprint of the content itself. Change
one character of the content and you get an entirely different ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pubkey&lt;/code&gt;&lt;/strong&gt; — the author's public key, in hex.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;created_at&lt;/code&gt;&lt;/strong&gt; — a Unix timestamp, set by the client, not the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;kind&lt;/code&gt;&lt;/strong&gt; — an integer that tells every relay and client what &lt;em&gt;type&lt;/em&gt; of event this is.
This single field is what lets the protocol stay generic while supporting wildly
different features — more on this below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tags&lt;/code&gt;&lt;/strong&gt; — an array of arrays, used for references, replies, mentions, hashtags, and
metadata specific to that event kind. This is the protocol's extensibility escape
hatch: instead of adding new top-level fields for every feature, features are
expressed through tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;content&lt;/code&gt;&lt;/strong&gt; — the actual payload, usually a string (sometimes itself JSON, sometimes
encrypted, depending on the kind).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sig&lt;/code&gt;&lt;/strong&gt; — a Schnorr signature over the event ID, produced with the author's private
key. Anyone, anywhere, can verify this signature using nothing but the public key and
the event data — no server round-trip required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That signature is the whole trust model. A relay could be hostile, compromised, or run by&lt;br&gt;
someone with terrible intentions, and it &lt;em&gt;still&lt;/em&gt; can't forge a post from you or alter&lt;br&gt;
your existing posts without breaking the signature — any client that checks it will&lt;br&gt;
immediately reject a tampered event.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.3 Kinds: the field that does all the work
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;kind&lt;/code&gt; number is genuinely the most important design decision in the whole protocol.&lt;br&gt;
A few of the most widely used:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Profile metadata (display name, avatar URL, NIP-05, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Short text note — the "tweet"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Contact list — who you follow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Encrypted DM (legacy, deprecated — see NIP-17 below)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Deletion request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Repost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Reaction (like, or an arbitrary emoji/vote)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1063&lt;/td&gt;
&lt;td&gt;File metadata&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9734 / 9735&lt;/td&gt;
&lt;td&gt;Zap request / zap receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30023&lt;/td&gt;
&lt;td&gt;Long-form article&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;39000-39009&lt;/td&gt;
&lt;td&gt;Group metadata (NIP-29)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kinds are grouped into ranges with different storage semantics — some are "regular"&lt;br&gt;
(every event is kept forever), some are "replaceable" (only the newest event per&lt;br&gt;
pubkey+kind is retained, useful for things like profile metadata that shouldn't&lt;br&gt;
accumulate history), and some are "ephemeral" (not stored at all, just relayed live,&lt;br&gt;
useful for typing indicators or presence). This range-based convention means a relay can&lt;br&gt;
implement sensible storage behavior for a brand-new kind it's never seen before, just by&lt;br&gt;
checking which numeric range it falls into.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.4 Relays are dumb pipes, not authorities
&lt;/h3&gt;

&lt;p&gt;A relay is nothing more than a WebSocket server that stores events and answers queries&lt;br&gt;
for them. That's the entire job description. Critically, a relay has &lt;em&gt;no&lt;/em&gt; special power&lt;br&gt;
over your identity — it can refuse to store your events (rate limiting, spam filtering,&lt;br&gt;
an outright ban from that specific relay), but it cannot delete your account, because&lt;br&gt;
there was never an account for it to control in the first place. Your identity lives in&lt;br&gt;
your keypair, not in any relay's database.&lt;/p&gt;

&lt;p&gt;In practice, clients publish the same event to several relays simultaneously — typically&lt;br&gt;
somewhere between three and ten. If one relay goes offline permanently, or bans you, or&lt;br&gt;
turns out to be run by someone acting in bad faith, your content and identity survive&lt;br&gt;
intact on every other relay you published to. This redundancy is the actual mechanism&lt;br&gt;
that makes the network censorship-resistant — not any single relay's goodwill, but the&lt;br&gt;
fact that no single relay is a point of failure.&lt;/p&gt;
&lt;h3&gt;
  
  
  3.5 Clients are interchangeable, by construction
&lt;/h3&gt;

&lt;p&gt;Damus, Primal, Amethyst, Ditto, YakiHonne, Iris, Coracle — these are all independently&lt;br&gt;
built apps, often by teams that have never talked to each other, reading and writing the&lt;br&gt;
exact same event format from the exact same relays. Switch from one to another mid-day&lt;br&gt;
and your followers, your posts, your DMs, and your Lightning wallet connection all show&lt;br&gt;
up automatically, because none of it was ever app-specific data — it's just events on&lt;br&gt;
relays, addressed by your pubkey.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. How the wire protocol actually works
&lt;/h2&gt;

&lt;p&gt;It's worth seeing the raw mechanics once, because it demystifies a lot of what "Nostr&lt;br&gt;
app" actually means under the hood. Everything happens over a WebSocket connection to a&lt;br&gt;
relay, using a small set of message types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Publishing an event&lt;/strong&gt; — the client sends:&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="s2"&gt;"EVENT"&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="err"&gt;...the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;signed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;object...&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;The relay responds with an &lt;code&gt;OK&lt;/code&gt; message confirming whether it accepted or rejected the&lt;br&gt;
event (and why, if rejected — e.g. rate-limited, invalid signature, blocked pubkey).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requesting events&lt;/strong&gt; — the client sends a subscription request with a filter:&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="s2"&gt;"REQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscription-id-1"&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;"kinds"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"authors"&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="s2"&gt;"91cf9...f857"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&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;The relay streams back matching events as &lt;code&gt;EVENT&lt;/code&gt; messages tagged with that subscription&lt;br&gt;
ID, followed by an &lt;code&gt;EOSE&lt;/code&gt; ("end of stored events") message once it's sent everything it&lt;br&gt;
currently has — after which the subscription stays open and the relay pushes any &lt;em&gt;new&lt;/em&gt;&lt;br&gt;
matching events live, in real time, as they arrive. This is how a Nostr feed actually&lt;br&gt;
updates without polling: it's a long-lived subscription over an open socket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closing a subscription:&lt;/strong&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="s2"&gt;"CLOSE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscription-id-1"&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;That's genuinely most of the protocol's mechanical surface. Everything more advanced —&lt;br&gt;
groups, zaps, encrypted DMs — is built by defining new event kinds and tag conventions on&lt;br&gt;
top of this same three-message exchange, not by adding new wire-level message types.&lt;/p&gt;
&lt;h2&gt;
  
  
  5. NIPs: the extension system
&lt;/h2&gt;

&lt;p&gt;The base spec, &lt;strong&gt;NIP-01&lt;/strong&gt;, only defines the event format and the REQ/EVENT/CLOSE&lt;br&gt;
exchange above. Everything else is an optional extension called a &lt;strong&gt;NIP&lt;/strong&gt; — a "Nostr&lt;br&gt;
Implementation Possibility," numbered and published in a public GitHub repository.&lt;br&gt;
Clients and relays adopt whichever NIPs make sense for what they're building; nothing is&lt;br&gt;
mandatory. This keeps the core protocol genuinely tiny — you can read NIP-01 in about ten&lt;br&gt;
minutes — while letting the surrounding ecosystem grow features without ever breaking&lt;br&gt;
backward compatibility, since old clients simply ignore kinds and tags they don't&lt;br&gt;
recognize.&lt;/p&gt;

&lt;p&gt;A few of the most consequential NIPs in production today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NIP-05&lt;/strong&gt; — maps a human-readable identifier (formatted like an email address, e.g.
&lt;code&gt;alice@example.com&lt;/code&gt;) to a pubkey via a &lt;code&gt;.well-known&lt;/code&gt; DNS lookup, so users see something
more recognizable than a wall of hex characters, and so impersonation is at least
partially mitigated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-17 / NIP-44&lt;/strong&gt; — properly encrypted, metadata-minimizing direct messages, using a
"gift wrap" scheme that hides even who's talking to whom. This replaced the earlier
&lt;strong&gt;NIP-04&lt;/strong&gt;, which encrypted message &lt;em&gt;content&lt;/em&gt; but left sender, recipient, and timing
fully visible to any relay — a real privacy hole for anything called an "encrypted" DM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-23&lt;/strong&gt; — long-form content: real articles with titles, summaries, and Markdown
bodies, rather than short notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-25&lt;/strong&gt; — reactions: likes, and by extension arbitrary up/down-style voting, since
the content field can hold any string, not just an emoji.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-29&lt;/strong&gt; — relay-based groups: the closest thing Nostr has to a "server" in the
Discord sense, with defined admin/moderator roles, membership lists, and channels, with
permission checks enforced by the relay hosting the group rather than trusted from the
client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-42&lt;/strong&gt; — relay authentication, needed for any access-gated or private content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-46&lt;/strong&gt; — remote signing (often called a "bunker"): lets an app request a signature
from a separate signer application, so the app itself never has direct access to your
raw private key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NIP-57&lt;/strong&gt; — zaps, covered in detail next.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Anyone can draft a NIP. Adoption is entirely organic — a NIP "succeeds" not because a&lt;br&gt;
governing body approves it, but because enough clients and relays independently decide&lt;br&gt;
it's worth implementing, the same evolutionary pressure that determines which HTTP&lt;br&gt;
headers or email extensions become universal over time.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. Zaps: the feature that makes Nostr different from everything before it
&lt;/h2&gt;

&lt;p&gt;ActivityPub solved federation over a decade ago. What it never solved — what no prior&lt;br&gt;
open social protocol solved — is native monetization. There's no built-in way to pay a&lt;br&gt;
creator inside ActivityPub itself; every attempt bolts on a separate payment processor,&lt;br&gt;
subscription platform, or ad network sitting outside the protocol.&lt;/p&gt;

&lt;p&gt;Nostr's answer, defined in &lt;strong&gt;NIP-57&lt;/strong&gt;, is called a &lt;strong&gt;zap&lt;/strong&gt;: a Lightning Network payment&lt;br&gt;
wrapped inside a signed Nostr event and attached directly to the specific post or profile&lt;br&gt;
it's paying. Mechanically, it works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You tap "zap" on a post. Your client constructs a &lt;code&gt;kind: 9734&lt;/code&gt; zap &lt;em&gt;request&lt;/em&gt; event,
referencing the post being zapped, and sends it to the recipient's configured
Lightning address.&lt;/li&gt;
&lt;li&gt;That Lightning address returns a BOLT11 invoice.&lt;/li&gt;
&lt;li&gt;Your wallet pays the invoice over the Lightning Network.&lt;/li&gt;
&lt;li&gt;The recipient's Lightning service provider publishes a &lt;code&gt;kind: 9735&lt;/code&gt; zap &lt;em&gt;receipt&lt;/em&gt;
event back to the relays, cryptographically proving the payment happened and linking
it to the original post.&lt;/li&gt;
&lt;li&gt;Every client that renders that post can now display the zap total, because the proof
of payment lives in the same event graph as the content itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The significant part isn't the mechanism — it's that the payment and the social action&lt;br&gt;
happen in the same protocol layer, with no ad network, no platform fee, and no separate&lt;br&gt;
payment processor mediating the relationship between creator and audience. This is the&lt;br&gt;
specific reason Bitcoin-native developers gravitate toward Nostr rather than other&lt;br&gt;
decentralized-social efforts: the social graph and the payment rail are literally the&lt;br&gt;
same underlying system, not two separate products stitched together.&lt;/p&gt;
&lt;h2&gt;
  
  
  7. Nostr vs. ActivityPub vs. Bluesky's AT Protocol
&lt;/h2&gt;

&lt;p&gt;Three protocols regularly get compared, so it's worth being precise about how they differ:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Nostr&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ActivityPub (Mastodon)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;AT Protocol (Bluesky)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity&lt;/td&gt;
&lt;td&gt;Cryptographic keypair, held by the user&lt;/td&gt;
&lt;td&gt;Handle tied to a specific server (&lt;code&gt;@you@server&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;DID (decentralized identifier), can be self-hosted or provider-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting model&lt;/td&gt;
&lt;td&gt;Any relay, freely swappable&lt;/td&gt;
&lt;td&gt;Federated servers, admin-controlled&lt;/td&gt;
&lt;td&gt;"Personal Data Servers," designed for portability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Moving providers&lt;/td&gt;
&lt;td&gt;Trivial — same key, any client/relay&lt;/td&gt;
&lt;td&gt;Difficult — handle and history are server-bound&lt;/td&gt;
&lt;td&gt;Supported by design, but less battle-tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Native payments&lt;/td&gt;
&lt;td&gt;Yes — zaps (NIP-57), Lightning-native&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol complexity&lt;/td&gt;
&lt;td&gt;Deliberately minimal&lt;/td&gt;
&lt;td&gt;Comparatively heavyweight (inherited from ActivityStreams)&lt;/td&gt;
&lt;td&gt;Moderate, with a stronger emphasis on algorithmic feeds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance&lt;/td&gt;
&lt;td&gt;No formal body; NIPs adopted organically&lt;/td&gt;
&lt;td&gt;W3C standard, more formal process&lt;/td&gt;
&lt;td&gt;Steered primarily by Bluesky PBC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these is strictly "better" in the abstract — they optimize for different things.&lt;br&gt;
ActivityPub optimizes for federation between admin-run communities. AT Protocol optimizes&lt;br&gt;
for algorithmic feed flexibility with portability as a design goal. Nostr optimizes for&lt;br&gt;
minimal trust surface and puts a payment rail directly into the protocol. Which one a&lt;br&gt;
given project should build on depends entirely on which of those properties actually&lt;br&gt;
matters for the problem being solved.&lt;/p&gt;
&lt;h2&gt;
  
  
  8. What real production apps look like on this stack
&lt;/h2&gt;

&lt;p&gt;It helps to look at two projects that made very different architectural choices on top&lt;br&gt;
of the exact same protocol:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ditto&lt;/strong&gt; is a self-hostable community server, written in Deno/TypeScript with Postgres&lt;br&gt;
for indexing. Rather than building a new UI from scratch, it implements Mastodon's REST&lt;br&gt;
API &lt;em&gt;on top of&lt;/em&gt; Nostr events — so any existing Mastodon-compatible client can point at a&lt;br&gt;
Ditto instance and just work, while the actual data underneath is portable Nostr events&lt;br&gt;
rather than server-locked ActivityPub records. It's aimed squarely at admins who want to&lt;br&gt;
run their own community without the deplatforming risk that comes with depending on one&lt;br&gt;
company's infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YakiHonne&lt;/strong&gt; took the opposite approach: rather than being self-hosted infrastructure,&lt;br&gt;
it's a polished client app (web, iOS, Android) with its own relay network built in&lt;br&gt;
strfry (C++), optimized for long-form publishing and creator monetization, with a&lt;br&gt;
built-in Lightning/Cashu wallet baked directly into the app. It also introduced &lt;strong&gt;Smart&lt;br&gt;
Widgets&lt;/strong&gt; — interactive mini-apps encoded as Nostr events — pushing the protocol beyond&lt;br&gt;
static posts into small embedded applications.&lt;/p&gt;

&lt;p&gt;Both are legitimate, production-grade approaches to the same underlying protocol, which&lt;br&gt;
is the point: Nostr isn't a single app you either use or don't, it's a substrate that&lt;br&gt;
different teams build genuinely different products on top of — the same way SMTP&lt;br&gt;
underlies both a self-hosted mail server and Gmail.&lt;/p&gt;
&lt;h2&gt;
  
  
  9. The honest tradeoffs and open problems
&lt;/h2&gt;

&lt;p&gt;Decentralization isn't free, and a fair explainer has to say so plainly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discovery is unsolved.&lt;/strong&gt; No company curates a global feed, which is exactly the
point, but it also means good content discovery has to be engineered by each client
independently, and most haven't fully cracked it yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Moderation is per-relay, not global.&lt;/strong&gt; A relay can reject spam or abusive content on
itself, but there's no way to remove something from the network entirely — it may
still exist on other relays. This is a deliberate consequence of the trust model, and
it means building real abuse-handling tooling is squarely the responsibility of anyone
running a relay or client, not something you inherit for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key management has no safety net.&lt;/strong&gt; There's no "forgot password" flow. Lose your
nsec and you've lost that identity permanently, unless the specific client you used
built its own backup/recovery scheme on top of the base protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No global consensus on state.&lt;/strong&gt; Things like a reaction count or a vote total are only
ever a view assembled from whichever relays your client happened to query — not a
single canonical number the way a centralized database would give you. Two different
clients can legitimately show two different totals for the same post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spam resistance is still maturing.&lt;/strong&gt; Because publishing an event is nearly free,
relays rely on rate limiting, proof-of-work stamps (NIP-13), paid relays, or
allowlists to keep spam manageable — there's no single dominant solution yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are protocol bugs exactly — they're the direct, unavoidable tradeoff for&lt;br&gt;
not having a company sitting in the middle making those decisions on your behalf.&lt;br&gt;
Building well on Nostr means designing around these constraints explicitly, rather than&lt;br&gt;
assuming the platform-style guarantees a centralized backend would otherwise give you for&lt;br&gt;
free.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. Getting started: a minimal working example
&lt;/h2&gt;

&lt;p&gt;If you want to see the protocol in action, here's the shortest path from zero to a&lt;br&gt;
published event using &lt;code&gt;nostr-tools&lt;/code&gt; (JavaScript/TypeScript):&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generateSecretKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;finalizeEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nostr-tools/pure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Relay&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nostr-tools/relay&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Generate a keypair (this is your "sign up")&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateSecretKey&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;pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 2. Construct and sign an event&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;finalizeEvent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello, nostr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. Publish it to a relay&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Relay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wss://relay.damus.io&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;relay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Published as&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a complete, working Nostr client in about a dozen lines — no server, no signup&lt;br&gt;
form, no API key. For anything user-facing, you'd swap step 1 for a &lt;strong&gt;NIP-07&lt;/strong&gt; browser&lt;br&gt;
extension (like Alby or nos2x) or a &lt;strong&gt;NIP-46&lt;/strong&gt; remote signer, so the app itself never&lt;br&gt;
touches the raw private key at all — a meaningful security practice, not just a nicety.&lt;/p&gt;

&lt;p&gt;Other useful entry points depending on your stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nostr-tools&lt;/code&gt;&lt;/strong&gt; (JS/TS) — the most widely used client-side library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;go-nostr&lt;/code&gt;&lt;/strong&gt; (Go) — solid choice for relay-side or backend infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;nostr-sdk&lt;/code&gt;&lt;/strong&gt; (Rust) — used by several higher-performance clients and relay
implementations&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/nostr-protocol/nips" rel="noopener noreferrer"&gt;NIPs repository on GitHub&lt;/a&gt; — the actual
living spec, genuinely readable in an afternoon since the core protocol is small by
design&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  11. Where this is heading
&lt;/h2&gt;

&lt;p&gt;The protocol's minimalism is also its growth strategy: because NIP-01 barely changes, the&lt;br&gt;
surface area for breaking compatibility stays small, while the NIP process lets the&lt;br&gt;
ecosystem experiment quickly at the edges — group chat, long-form publishing, Lightning&lt;br&gt;
payments, and interactive widgets have all been added this way without a single rewrite&lt;br&gt;
of the base spec. That's a genuinely different development model from most protocols,&lt;br&gt;
which tend to accumulate complexity in the core spec itself over time.&lt;/p&gt;

&lt;p&gt;The honest way to think about where Nostr fits: it's not trying to replace every social&lt;br&gt;
platform overnight. It's a substrate — a shared identity and event layer — that lets&lt;br&gt;
independent developers build interoperable products without needing anyone's permission&lt;br&gt;
or API key to do it. Whether that ends up mattering at large scale is still an open&lt;br&gt;
question the ecosystem is actively answering in public, one NIP and one client at a time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're building on Nostr and want to compare notes on relay architecture, NIP-29&lt;br&gt;
groups, or Lightning/Cashu integration, drop a comment below — always interested in what&lt;br&gt;
people are actually shipping on this stack.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>nostr</category>
      <category>bitcoin</category>
      <category>opensource</category>
    </item>
    <item>
      <title>12 GitHub Actions Workflows That Will Quietly Save Your DevOps Team Hours Every Week</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:35:11 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/12-github-actions-workflows-that-will-quietly-save-your-devops-team-hours-every-week-2j6d</link>
      <guid>https://dev.to/amayo_clinton/12-github-actions-workflows-that-will-quietly-save-your-devops-team-hours-every-week-2j6d</guid>
      <description>&lt;p&gt;If your team is still manually running tests, tagging releases, or nudging people about stale PRs — you're leaving a lot of free automation on the table. GitHub Actions has matured into a genuinely capable automation layer that goes way beyond "run tests on push," and most teams only scratch the surface of it.&lt;/p&gt;

&lt;p&gt;This is a practical rundown of automation patterns that actually pull weight in production, organized by the problem they solve rather than as a generic feature list. Copy-paste, adapt, ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CI/CD That Actually Gates Deployments
&lt;/h2&gt;

&lt;p&gt;The baseline pattern everyone starts with, but worth doing properly: don't just run tests — make deployment &lt;em&gt;conditional&lt;/em&gt; on them passing, and separate build/test from deploy into distinct jobs so you get clean status checks on PRs.&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;CI/CD Pipeline&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;build-and-test&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-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;npm'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build-and-test&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trade-off worth knowing:&lt;/strong&gt; &lt;code&gt;npm ci&lt;/code&gt; instead of &lt;code&gt;npm install&lt;/code&gt; in CI — it's stricter (fails if &lt;code&gt;package-lock.json&lt;/code&gt; is out of sync) and faster since it skips dependency resolution. Small change, meaningfully more reliable builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Linting and Static Analysis as a Required Check
&lt;/h2&gt;

&lt;p&gt;Catching style and correctness issues before a human reviewer has to. The key move here isn't just running the linter — it's making it a &lt;strong&gt;required status check&lt;/strong&gt; in your branch protection rules so it can't be merged around.&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;Lint&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&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;lint&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-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx eslint . --max-warnings=0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--max-warnings=0&lt;/code&gt; is doing a lot of work there — without it, warnings pile up silently and the check stays green forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Stale Issue/PR Triage
&lt;/h2&gt;

&lt;p&gt;Backlogs rot. Automating the "is this still relevant?" nudge keeps your issue tracker honest without anyone having to manually sweep it.&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;Stale Triage&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&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;stale&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/stale@v9&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;days-before-stale&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
          &lt;span class="na"&gt;days-before-close&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
          &lt;span class="na"&gt;exempt-issue-labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pinned,security'&lt;/span&gt;
          &lt;span class="na"&gt;stale-issue-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="s"&gt;This issue hasn't seen activity in 30 days and will close in 7&lt;/span&gt;
            &lt;span class="s"&gt;days if there's no update.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pro tip: always carve out an &lt;code&gt;exempt-issue-labels&lt;/code&gt; list. Nothing kills trust in this automation faster than a security issue auto-closing because nobody commented in a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Dependency Updates Without the Manual Review Bottleneck
&lt;/h2&gt;

&lt;p&gt;Dependabot (or Renovate) opens the PRs. The real automation win is &lt;strong&gt;safely&lt;/strong&gt; auto-merging the low-risk ones — patch/minor bumps that pass CI — while still gating major version bumps for human review.&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;Dependabot Auto-Merge&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull-requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;auto-merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.actor == 'dependabot[bot]'&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;dependabot/fetch-metadata@v2&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;metadata&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.metadata.outputs.update-type != 'version-update:semver-major'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gh pr merge --auto --squash "$PR_URL"&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PR_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.pull_request.html_url }}&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't skip this detail:&lt;/strong&gt; gate on &lt;code&gt;update-type&lt;/code&gt;. Blanket auto-merging &lt;em&gt;everything&lt;/em&gt; Dependabot opens, including majors, is how you wake up to a broken build from an unannounced breaking change.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Secret Scanning and Dependency Vulnerability Checks
&lt;/h2&gt;

&lt;p&gt;This is the one teams skip until it bites them. Two layers worth running on every push: scanning for accidentally committed secrets, and auditing dependencies for known CVEs.&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;Security Checks&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&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;audit&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="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Scan for secrets&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;trufflesecurity/trufflehog@main&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;extra_args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;--only-verified&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-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm audit --audit-level=high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--audit-level=high&lt;/code&gt; matters here — running &lt;code&gt;npm audit&lt;/code&gt; with no threshold means low-severity noise eventually gets ignored entirely, and then so does everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Release Automation with Auto-Generated Changelogs
&lt;/h2&gt;

&lt;p&gt;Manually writing release notes for every tag is exactly the kind of toil automation exists to kill. &lt;code&gt;release-drafter&lt;/code&gt; builds a changelog from merged PR titles/labels as you go, so cutting a release is a one-click action instead of an afternoon.&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;Release&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&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;release&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gh release create "${{ github.ref_name }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--title "Release ${{ github.ref_name }}" \&lt;/span&gt;
            &lt;span class="s"&gt;--generate-notes&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--generate-notes&lt;/code&gt; is a built-in GitHub CLI feature now — no third-party action required for basic changelog generation from PR history.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Infrastructure as Code: Plan on PR, Apply on Merge
&lt;/h2&gt;

&lt;p&gt;The pattern that matters here isn't "run Terraform in CI" — it's &lt;strong&gt;separating plan and apply&lt;/strong&gt; so reviewers can see exactly what infrastructure change they're approving before it happens.&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;Terraform&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;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&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;terraform&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;hashicorp/setup-terraform@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform init&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=tfplan&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name == 'pull_request'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform show -no-color tfplan&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main' &amp;amp;&amp;amp; github.event_name == 'push'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform apply -auto-approve tfplan&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applying from a &lt;em&gt;saved plan&lt;/em&gt; (&lt;code&gt;tfplan&lt;/code&gt;) rather than re-running &lt;code&gt;terraform apply&lt;/code&gt; blind is the difference between "what you reviewed is what got applied" and "what got applied is whatever the state drifted to by the time the apply job ran."&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Code Coverage Enforcement, Not Just Reporting
&lt;/h2&gt;

&lt;p&gt;Uploading a coverage report is easy. Making coverage &lt;em&gt;regressions&lt;/em&gt; actually block a merge is where the value is.&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;Coverage&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&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;test&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-node@v4&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;20'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run coverage&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;codecov/codecov-action@v4&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;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CODECOV_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;fail_ci_if_error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair this with Codecov's (or Coveralls') PR-comment integration and a minimum-coverage-delta rule in your repo settings — the report alone changes nothing unless someone's forced to look at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Scheduled Database Migration Checks
&lt;/h2&gt;

&lt;p&gt;Rather than blindly running migrations on every deploy (risky), a safer pattern is validating that migrations apply cleanly against a fresh database snapshot as part of CI — catching migration bugs before they hit staging.&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 Check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pull_request&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;migrate-check&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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5432:5432'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
          &lt;span class="s"&gt;--health-cmd pg_isready&lt;/span&gt;
          &lt;span class="s"&gt;--health-interval 10s&lt;/span&gt;
          &lt;span class="s"&gt;--health-timeout 5s&lt;/span&gt;
          &lt;span class="s"&gt;--health-retries 5&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run migrate&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres://postgres:postgres@localhost:5432/postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using a &lt;code&gt;services:&lt;/code&gt; container instead of a separately provisioned test DB keeps this fast and fully isolated — every run gets a clean Postgres instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Scheduled Backups With Verified Upload
&lt;/h2&gt;

&lt;p&gt;Cron-triggered workflows are underused for anything beyond stale-issue bots. Scheduled backups with a verified upload step are a good example of "boring automation" that actually matters when you need it.&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;Nightly Backup&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&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;backup&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backup.sh&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;google-github-actions/upload-cloud-storage@v2&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;backups/&lt;/span&gt;
          &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-backup-bucket/${{ github.run_id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag uploads with &lt;code&gt;github.run_id&lt;/code&gt; (as above) so every backup run is traceable back to exactly which workflow execution produced it — invaluable when you're debugging "which backup is this."&lt;/p&gt;

&lt;h2&gt;
  
  
  11. Slack Notifications Scoped to Signal, Not Noise
&lt;/h2&gt;

&lt;p&gt;Notifying on &lt;em&gt;every&lt;/em&gt; PR event trains people to ignore the channel within a week. Scope notifications to events that actually need a human's attention — failed deploys, merged-to-main, security audit failures.&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;Notify on Deploy Failure&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;workflow_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;CI/CD&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Pipeline'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;completed&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;notify&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.workflow_run.conclusion == 'failure'&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;slackapi/slack-github-action@v1.27.0&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;channel-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deploys'&lt;/span&gt;
          &lt;span class="na"&gt;slack-message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;🚨&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Deploy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;main&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;—&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;workflow&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run.'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SLACK_BOT_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;workflow_run&lt;/code&gt; with a &lt;code&gt;conclusion == 'failure'&lt;/code&gt; filter, rather than hooking every &lt;code&gt;pull_request&lt;/code&gt; event, is what keeps this useful instead of becoming background noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  12. Project Board Sync
&lt;/h2&gt;

&lt;p&gt;Auto-adding new issues to a project board removes the "someone forgot to triage this" failure mode entirely.&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;Add to Project Board&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;issues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&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;add-to-project&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/add-to-project@v1.0.2&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;project-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/orgs/your-org/projects/1&lt;/span&gt;
          &lt;span class="na"&gt;github-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROJECT_TOKEN }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note this needs a token with &lt;code&gt;project&lt;/code&gt; scope — the default &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; won't have permission to write to org-level Projects, which trips people up the first time they try this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The So-What
&lt;/h2&gt;

&lt;p&gt;None of these individually are groundbreaking — that's the point. The value compounds when you stack several of them: gated CI/CD, auto-merged safe dependency bumps, enforced coverage, and scoped notifications together mean your team stops doing manual toil and starts only getting pulled in when something actually needs a human decision.&lt;/p&gt;

&lt;p&gt;A few general principles that apply across all of the above:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer built-in tooling over third-party actions when it exists&lt;/strong&gt; (&lt;code&gt;gh release create --generate-notes&lt;/code&gt; over a dedicated release-notes action, for example) — fewer external dependencies in your CI supply chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin action versions&lt;/strong&gt;, and update them — several widely-copied examples floating around still reference &lt;code&gt;actions/checkout@v2&lt;/code&gt; and &lt;code&gt;setup-node@v1&lt;/code&gt;, both several major versions behind and missing performance/security improvements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate, don't just report.&lt;/strong&gt; A check that runs but doesn't block anything is documentation, not automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Discussion: What's the automation in your GitHub Actions setup that's saved you the most real time — and on the flip side, what's the one you set up that turned out to be more noise than signal? Curious what patterns people have actually kept versus ripped out after a few months.&lt;/p&gt;

</description>
      <category>github</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Fallback Password: Dissecting the Tenda Router Backdoor (CVE-2026-11405)</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:20:54 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/the-fallback-password-dissecting-the-tenda-router-backdoor-cve-2026-11405-kgl</link>
      <guid>https://dev.to/amayo_clinton/the-fallback-password-dissecting-the-tenda-router-backdoor-cve-2026-11405-kgl</guid>
      <description>&lt;p&gt;If you've ever reviewed authentication code and felt a little itch when you saw an &lt;code&gt;if (authFailed) { tryAnotherWay() }&lt;/code&gt; branch — congratulations, your instincts are correct. That exact pattern is at the center of a newly disclosed backdoor affecting multiple Tenda router models, and it's a great case study in how &lt;em&gt;not&lt;/em&gt; to design an auth flow.&lt;/p&gt;

&lt;p&gt;Let's break down what's actually happening in the firmware, why it matters beyond "yet another router CVE," and how you'd go looking for something like this yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TL;DR
&lt;/h2&gt;

&lt;p&gt;CERT/CC (Carnegie Mellon's Software Engineering Institute) published &lt;strong&gt;VU#213560 / CVE-2026-11405&lt;/strong&gt; on July 6, 2026, describing an undocumented authentication bypass in the &lt;code&gt;/bin/httpd&lt;/code&gt; binary shipped on several Tenda router firmware builds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;US_FH1201V1.0BR_V1.2.0.14(408)_EN_TD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;US_W15EV1.0br_V15.11.0.5(1068_1567_841)_EN_TDE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;US_AC10V1.0re_V15.03.06.46_multi_TDE01&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;US_AC5V1.0RTL_V15.03.06.48_multi_TDE01&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;US_AC6V2.0RTL_V15.03.06.51_multi_T&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No patch is available. CERT/CC says it notified Tenda privately on May 19, 2026, and got silence for seven weeks before disclosing publicly. Tenda has a documented history of not responding to vulnerability reports going back to 2013.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Vulnerable Logic
&lt;/h2&gt;

&lt;p&gt;Reconstructing the flow described in the advisory, the &lt;code&gt;login()&lt;/code&gt; function in &lt;code&gt;/bin/httpd&lt;/code&gt; looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudocode reconstruction based on CERT/CC's description&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Primary path: standard MD5-hashed credential check&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;stored_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_stored_password_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;submitted_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// via check_rand_key / PasswordToMd5&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;submitted_hash&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;stored_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;grant_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROLE_ADMIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// --- This is the backdoor ---&lt;/span&gt;
    &lt;span class="c1"&gt;// Fallback path: only reached when normal auth fails&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;backdoor_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sys.rzadmin.password"&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="n"&gt;strcmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backdoor_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Note: username is NEVER checked here&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;grant_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rzadmin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROLE_ADMIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// role=2&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;AUTH_FAILED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things jump out immediately if you're reading this as a developer:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It's not a &lt;em&gt;hardcoded&lt;/em&gt; credential — it's a fallback credential
&lt;/h3&gt;

&lt;p&gt;Most classic router backdoors (Zyxel's &lt;code&gt;zyfwp&lt;/code&gt;, Fortinet's SSH backdoor from 2019, various D-Link/ASUS incidents) work by embedding a &lt;strong&gt;static, hardcoded username/password pair&lt;/strong&gt; that's compiled into the binary or listed in &lt;code&gt;/etc/passwd&lt;/code&gt;. You can find those with a simple &lt;code&gt;strings&lt;/code&gt; pass or a known-credential wordlist.&lt;/p&gt;

&lt;p&gt;This one's architecturally sneakier: the backdoor password isn't hardcoded in plaintext in the binary — it's read at runtime from a config key (&lt;code&gt;sys.rzadmin.password&lt;/code&gt;) via &lt;code&gt;GetValue()&lt;/code&gt;. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It could theoretically be &lt;em&gt;different per device/batch&lt;/em&gt; if that config value is set during manufacturing/provisioning&lt;/li&gt;
&lt;li&gt;It's harder to spot via static binary analysis alone — you need to trace the &lt;code&gt;GetValue()&lt;/code&gt; call and understand where that NVRAM/config key gets populated&lt;/li&gt;
&lt;li&gt;It smells like a leftover engineering/support mechanism (remote diagnostics, QA testing) rather than a "let's plant a backdoor" decision — though intent doesn't really matter once it's shipped in production firmware&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Two authentication failures stacked on top of each other
&lt;/h3&gt;

&lt;p&gt;It's worth noting MD5 for password verification is already considered broken for anything security-sensitive — it's fast to brute-force and has known collision weaknesses. So even &lt;em&gt;before&lt;/em&gt; you get to the backdoor, the "legitimate" auth path is using deprecated crypto. The backdoor is really a second, more severe failure layered on top of a first one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No username validation = every account is the admin account
&lt;/h3&gt;

&lt;p&gt;This is the detail that pushes this from "bad" to "critical." Because &lt;code&gt;strcmp()&lt;/code&gt; only checks the password against the backdoor value and never validates &lt;code&gt;username&lt;/code&gt;, you don't need to know a valid admin username. &lt;code&gt;admin&lt;/code&gt;, &lt;code&gt;root&lt;/code&gt;, &lt;code&gt;asdf&lt;/code&gt;, empty string — doesn't matter. If you have the backdoor password, you're in with &lt;code&gt;role=2&lt;/code&gt; (root-equivalent access to the web management daemon).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Sits in the CVSS/Exploitability Landscape
&lt;/h2&gt;

&lt;p&gt;If you triage CVEs for a living, this one checks every box for "fix now, ask questions later":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Attack vector&lt;/strong&gt;: Network (no local/physical access needed if remote management is exposed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privileges required&lt;/strong&gt;: None&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User interaction&lt;/strong&gt;: None&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication bypass&lt;/strong&gt;: Complete — this &lt;em&gt;is&lt;/em&gt; the authentication mechanism failing open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Impact&lt;/strong&gt;: Full administrative control — config changes, DNS/routing manipulation, disabling security features, pivoting into the LAN&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The only mitigating factor right now is that &lt;strong&gt;no public PoC or confirmed active exploitation&lt;/strong&gt; has been reported as of this writing. That window won't stay open long — once someone extracts the exact backdoor password (or the algorithm that derives it) from firmware, this becomes a mass-scannable vulnerability.&lt;/p&gt;

&lt;h2&gt;
  
  
  How You'd Actually Find Something Like This
&lt;/h2&gt;

&lt;p&gt;If you want to understand the discovery process (or audit your own embedded/IoT firmware for similar issues), the general workflow researchers use looks like:&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="c"&gt;# 1. Extract the firmware filesystem&lt;/span&gt;
binwalk &lt;span class="nt"&gt;-e&lt;/span&gt; firmware_image.bin

&lt;span class="c"&gt;# 2. Locate the web server binary&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"httpd"&lt;/span&gt;

&lt;span class="c"&gt;# 3. Pull printable strings, looking for suspicious config keys&lt;/span&gt;
strings ./bin/httpd | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s2"&gt;"password|admin|rzadmin|backdoor"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Disassemble around the login() function&lt;/span&gt;
&lt;span class="c"&gt;# (Ghidra or IDA Free work well for MIPS/ARM router binaries)&lt;/span&gt;
&lt;span class="c"&gt;# Look for comparison logic that runs *after* a failed primary auth check —&lt;/span&gt;
&lt;span class="c"&gt;# especially calls to GetValue()/NVRAM-style config getters followed by&lt;/span&gt;
&lt;span class="c"&gt;# strcmp()/memcmp() rather than a hashed comparison&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The giveaway pattern to grep for in disassembly: a hash-based comparison (&lt;code&gt;md5&lt;/code&gt;, &lt;code&gt;sha&lt;/code&gt;) followed conditionally by a &lt;strong&gt;plaintext&lt;/strong&gt; &lt;code&gt;strcmp()&lt;/code&gt; against a runtime-fetched value. Legitimate fallback/recovery auth (e.g., factory reset flows) usually requires a physical trigger (button press, serial console) — not a remote HTTP request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection If You're Running Fleets of These Devices
&lt;/h2&gt;

&lt;p&gt;If you manage a bunch of Tenda hardware (MSPs, this means you), here's a rough network-level check while waiting on a patch:&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="c"&gt;# Confirm whether remote/WAN management is exposed at all —&lt;/span&gt;
&lt;span class="c"&gt;# this is your actual blast-radius question&lt;/span&gt;
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 80,443,8080 &amp;lt;router-ip&amp;gt;

&lt;span class="c"&gt;# If reachable, check for the login endpoint and response behavior&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; http://&amp;lt;router-ip&amp;gt;/login.cgi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can't test for the specific backdoor password without it being disclosed (responsible security practice, and also nobody's published a PoC), but you &lt;em&gt;can&lt;/em&gt; and should verify whether the management interface is internet-facing at all. That single control point — WAN-facing admin panel, yes/no — determines whether this bug is "theoretical" or "actively scannable" for your deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigations (Until Tenda Ships a Fix, If Ever)
&lt;/h2&gt;

&lt;p&gt;CERT/CC's guidance, plus some practical additions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Disable remote/WAN web management&lt;/strong&gt; — this is the single highest-leverage fix. It doesn't close the backdoor, but it removes it from the internet-facing attack surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change the default LAN IP range&lt;/strong&gt; — reduces opportunistic discovery by automated scanners hunting default &lt;code&gt;192.168.x.1&lt;/code&gt;-style ranges. Doesn't stop targeted attackers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segment IoT/consumer hardware onto its own VLAN&lt;/strong&gt; — if this router is sitting in a business environment, don't let it share a broadcast domain with anything sensitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat "no vendor response since 2013" as a procurement signal&lt;/strong&gt; — Tenda's disclosure history (2013, 2020, 2021, 2022, now 2026) is itself a data point worth weighing against price when selecting network hardware for anything beyond disposable home use.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  So What? (The Actionable Bit)
&lt;/h2&gt;

&lt;p&gt;If you're building or reviewing embedded auth code, the concrete lesson here isn't "don't use backdoors" — nobody's shipping this intentionally as a "feature" in the marketing sense. It's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any fallback authentication path is a second attack surface.&lt;/strong&gt; If you have a "primary check fails, try secondary check" pattern anywhere in your auth flow — for support tooling, factory testing, recovery — that secondary path needs the &lt;em&gt;same&lt;/em&gt; scrutiny (and ideally the same removal-before-shipping discipline) as the primary one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config-driven secrets aren't automatically safer than hardcoded ones.&lt;/strong&gt; They're harder to spot via static &lt;code&gt;strings&lt;/code&gt; analysis, sure — but that's a detection problem, not a security improvement. If anything, it makes audits harder without making the underlying risk smaller.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username validation isn't optional, ever&lt;/strong&gt;, even in a fallback/debug path. "Any username + secret password = admin" is a strictly worse design than "specific username + secret password," and it costs nothing to enforce.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've got Tenda hardware anywhere in your stack — home lab, MSP client site, that one office router nobody's touched since 2022 — this is worth 10 minutes today to check exposure, even without a patch to apply.&lt;/p&gt;

&lt;p&gt;Have you found similar "fallback auth" patterns during firmware audits or code reviews — intentional debug backdoors that just never got stripped before shipping? Where do you draw the line between "reasonable engineering shortcut" and "should never have existed in production"? Curious how other embedded/security folks think about auditing this class of bug systematically rather than one CVE at a time.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>networking</category>
      <category>security</category>
      <category>infosec</category>
    </item>
    <item>
      <title>Git Mastery</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:37:47 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/git-mastery-40ee</link>
      <guid>https://dev.to/amayo_clinton/git-mastery-40ee</guid>
      <description>&lt;p&gt;If you think you know Git because you can &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, and &lt;code&gt;push&lt;/code&gt;, try this: build a single &lt;code&gt;hello.sh&lt;/code&gt; script through every core Git concept — commits, history rewriting, tagging, branching, conflicts, rebasing, bare repos — and document every command as you go.&lt;/p&gt;

&lt;p&gt;That's exactly what this &lt;strong&gt;Git project&lt;/strong&gt; puts you through. It's deceptively simple on paper ("just print Hello World") and brutally thorough underneath. Here's the walkthrough, built from my actual terminal output and commit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"devuser"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"devuser@example.com"&lt;/span&gt;

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; work/hello &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;work/hello
git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing fancy — just a repo and a script:&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="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello, World"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hello.sh
git add hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add initial hello world script"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first commit (&lt;code&gt;d03dea2&lt;/code&gt;) is the root of everything that follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building History On Purpose
&lt;/h2&gt;

&lt;p&gt;The project pushes you to think about commits as &lt;em&gt;units of meaning&lt;/em&gt;, not just checkpoints. When I added a shebang and comments to &lt;code&gt;hello.sh&lt;/code&gt;, I didn't just commit the whole diff — I split it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nt"&gt;-p&lt;/span&gt; hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: add default value comment"&lt;/span&gt;
&lt;span class="c"&gt;# commit: 7fd0362&lt;/span&gt;

git add hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refactor: use variable with default fallback"&lt;/span&gt;
&lt;span class="c"&gt;# commit: a53609a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git add -p&lt;/code&gt; is the star here — it lets you stage &lt;em&gt;parts&lt;/em&gt; of a file's changes, so a single edit to &lt;code&gt;hello.sh&lt;/code&gt; becomes two logically separate commits instead of one messy one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading history the useful way
&lt;/h3&gt;

&lt;p&gt;Plain &lt;code&gt;git log&lt;/code&gt; is fine, but for real work you want condensed, filtered, custom views:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;          &lt;span class="c"&gt;# condensed&lt;/span&gt;
git log &lt;span class="nt"&gt;-2&lt;/span&gt;                 &lt;span class="c"&gt;# last 2 entries&lt;/span&gt;
git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"5 minutes ago"&lt;/span&gt;   &lt;span class="c"&gt;# time-scoped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a genuinely useful personalized format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;format:&lt;span class="s2"&gt;"* %h %ad | %s%d [%an]"&lt;/span&gt; &lt;span class="nt"&gt;--date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;short
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;* d0dd395 2026-04-23 | docs: restore README and move documentation to README_REPORT.md (HEAD -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;main, origin/main, origin/HEAD&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;devuser]
&lt;span class="go"&gt;* 7d6d6db 2026-04-22 | docs: add complete project documentation with hashes [devuser]
* 0a72da5 2026-04-22 | docs: update README for shared repo [devuser]
* 237cb11 2026-04-22 | docs: add comment to Makefile (origin/greet) [devuser]
* a53609a 2026-04-21 | refactor: use variable with default fallback (tag: v1) [devuser]
* 7fd0362 2026-04-21 | docs: add default value comment (tag: v1-beta) [devuser]
* d03dea2 2026-04-21 | feat: add initial hello world script [devuser]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, all the context you need: hash, date, message, refs, author.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time Travel: Checkout, Tags, and Not Losing Your Mind
&lt;/h2&gt;

&lt;p&gt;Restoring old snapshots is where &lt;code&gt;git checkout &amp;lt;hash&amp;gt;&lt;/code&gt; earns its keep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout d03dea2
&lt;span class="nb"&gt;cat &lt;/span&gt;hello.sh
&lt;span class="c"&gt;# echo "Hello, World"&lt;/span&gt;

git checkout 7fd0362
&lt;span class="nb"&gt;cat &lt;/span&gt;hello.sh
&lt;span class="c"&gt;# #!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# # Default is "World"&lt;/span&gt;
&lt;span class="c"&gt;# echo "Hello, $1"&lt;/span&gt;

git checkout master   &lt;span class="c"&gt;# back to the tip, no hash memorized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tags turn "that one commit" into a name you'll actually remember:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag v1                  &lt;span class="c"&gt;# tags a53609a — current version&lt;/span&gt;
git tag v1-beta HEAD~1       &lt;span class="c"&gt;# tags 7fd0362 — one commit back, no hash needed&lt;/span&gt;

git tag
&lt;span class="c"&gt;# v1&lt;/span&gt;
&lt;span class="c"&gt;# v1-beta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jumping between &lt;code&gt;v1&lt;/code&gt; and &lt;code&gt;v1-beta&lt;/code&gt; is just &lt;code&gt;git checkout &amp;lt;tagname&amp;gt;&lt;/code&gt; — tags behave like read-only bookmarks into history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing Your Mind (Repeatedly)
&lt;/h2&gt;

&lt;p&gt;This section of the project is really about the difference between &lt;strong&gt;unstaged&lt;/strong&gt;, &lt;strong&gt;staged&lt;/strong&gt;, and &lt;strong&gt;committed&lt;/strong&gt; — and the different tool for undoing each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git restore hello.sh              &lt;span class="c"&gt;# discard unstaged edits&lt;/span&gt;
git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; hello.sh     &lt;span class="c"&gt;# unstage (keep the edits)&lt;/span&gt;
git revert HEAD &lt;span class="nt"&gt;--no-edit&lt;/span&gt;         &lt;span class="c"&gt;# undo a commit by creating a new one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it gets fun: tag a commit, reset past it, and prove the "deleted" commit still exists until Git actually garbage-collects it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag oops              &lt;span class="c"&gt;# tags 1cff52e&lt;/span&gt;
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; v1       &lt;span class="c"&gt;# HEAD now back at a53609a&lt;/span&gt;

git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;span class="c"&gt;# 1cff52e is STILL visible — reset doesn't delete objects, it just moves refs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To actually make it disappear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog expire &lt;span class="nt"&gt;--expire&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now &lt;span class="nt"&gt;--all&lt;/span&gt;
git gc &lt;span class="nt"&gt;--prune&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the moment the project really teaches you something most tutorials skip: &lt;strong&gt;&lt;code&gt;git reset&lt;/code&gt; doesn't delete data, it just moves pointers.&lt;/strong&gt; Nothing is gone until the reflog expires and garbage collection runs.&lt;/p&gt;

&lt;p&gt;One more subtlety — fixing a mistake &lt;em&gt;without&lt;/em&gt; creating a new commit:&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="c"&gt;# forgot the author email — fix the file, then fold it into the last commit&lt;/span&gt;
git add hello.sh
git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-edit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--amend&lt;/code&gt; rewrites the last commit in place instead of adding a new one. Great for local fixes, dangerous on anything already pushed and shared.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reorganizing the Project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;lib
git &lt;span class="nb"&gt;mv &lt;/span&gt;hello.sh lib/hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: move hello.sh into lib/ directory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git mv&lt;/code&gt; does the &lt;code&gt;mv&lt;/code&gt; and the &lt;code&gt;git add&lt;/code&gt; in one step — Git tracks it as a rename, not a delete+add, as long as the content is similar enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Under the Hood: Blobs, Trees, Commits
&lt;/h2&gt;

&lt;p&gt;This is the part that actually explains &lt;em&gt;why&lt;/em&gt; Git is fast and safe: everything is content-addressed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git cat-file &lt;span class="nt"&gt;-t&lt;/span&gt; 0a72da543f
&lt;span class="c"&gt;# commit&lt;/span&gt;

git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; 0a72da543f
&lt;span class="c"&gt;# tree   74c47aeda48e094725bd8ae1d62adbbc88997e1b&lt;/span&gt;
&lt;span class="c"&gt;# parent 980a0b51b84a5305bffdfbacd1f266bd8e9fa3a3&lt;/span&gt;
&lt;span class="c"&gt;# author devuser &amp;lt;devuser@example.com&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;# docs: update README for shared repo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A commit object is just a pointer to a &lt;strong&gt;tree&lt;/strong&gt; (a snapshot of the directory) plus a &lt;strong&gt;parent&lt;/strong&gt; (the previous commit) plus metadata. Walk the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-tree HEAD
&lt;span class="c"&gt;# 100644 blob 12d4ffb...  Makefile&lt;/span&gt;
&lt;span class="c"&gt;# 100644 blob 0791b4b...  README.md&lt;/span&gt;
&lt;span class="c"&gt;# 040000 tree b47b7e2...  lib&lt;/span&gt;

git ls-tree HEAD lib/
&lt;span class="c"&gt;# 100644 blob 2d14ca5...  lib/greeter.sh&lt;/span&gt;
&lt;span class="c"&gt;# 100644 blob c282d10...  lib/hello.sh&lt;/span&gt;

git cat-file &lt;span class="nt"&gt;-p&lt;/span&gt; c282d107dea0aa9909c27c291de8a83182847034
&lt;span class="c"&gt;# (raw content of lib/hello.sh)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every version of every file ever committed lives in &lt;code&gt;.git/objects/&lt;/code&gt; as a blob, addressed by the SHA-1 hash of its content. &lt;code&gt;HEAD&lt;/code&gt; just points at a branch ref, which points at a commit, which points at a tree, which points at blobs. That's the whole model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Branching Without Fear
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; greet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built out a &lt;code&gt;Greeter&lt;/code&gt; function on its own branch, isolated from &lt;code&gt;master&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;git add lib/greeter.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add Greeter function in lib/greeter.sh"&lt;/span&gt;
&lt;span class="c"&gt;# 0f958cb&lt;/span&gt;

git add lib/hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refactor: use Greeter function in hello.sh"&lt;/span&gt;

git add Makefile
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: add comment to Makefile"&lt;/span&gt;
&lt;span class="c"&gt;# 237cb11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And diffed the two branches directly, file by file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff master greet &lt;span class="nt"&gt;--&lt;/span&gt; Makefile
git diff master greet &lt;span class="nt"&gt;--&lt;/span&gt; lib/hello.sh
git diff master greet &lt;span class="nt"&gt;--&lt;/span&gt; lib/greeter.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resulting graph, right before merging things back together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;* 237cb11 (origin/greet) docs: add comment to Makefile
* 0f958cb feat: add Greeter function in lib/greeter.sh
* ef6052e feat: add interactive name prompt in hello.sh
* 7ee3abd docs: add README
* 45087c6 chore: add Makefile with run target
* 90ca44f chore: move hello.sh into lib/ directory
* 1a2643c docs: add author comment
| * 1cff52e (tag: oops) chore unwanted change to be removed
| * 7fd9565 Revert "chore: add unwanted committed change"
| * fb95a77 chore: add unwanted committed change
|/
* a53609a (tag: v1) refactor: use variable with default fallback
* 7fd0362 (tag: v1-beta) docs: add default value comment
* 9562a00 feat: add shebang and accept name argument
* d03dea2 feat: add initial hello world script
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can actually see the &lt;code&gt;oops&lt;/code&gt; line branching off and rejoining — a nice visual proof that &lt;code&gt;reset&lt;/code&gt; didn't destroy anything, it just left that commit orphaned until GC swept it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Main Event: Conflicts and Rebasing
&lt;/h2&gt;

&lt;p&gt;This is where the project stops being polite. First, a clean merge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch greet
git merge master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I changed &lt;code&gt;hello.sh&lt;/code&gt; on &lt;code&gt;master&lt;/code&gt; itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch master
git add lib/hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add interactive name prompt in hello.sh"&lt;/span&gt;
&lt;span class="c"&gt;# ef6052e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now merge &lt;code&gt;master&lt;/code&gt; into &lt;code&gt;greet&lt;/code&gt; again — except this time both branches touched the same lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch greet
git merge master
&lt;span class="c"&gt;# CONFLICT (content): Merge conflict in lib/hello.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resolving it by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nano lib/hello.sh   &lt;span class="c"&gt;# remove conflict markers, keep master's version&lt;/span&gt;
git add lib/hello.sh
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix: resolve merge conflict accepting master version"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Then: undo the merge and rebase instead
&lt;/h3&gt;

&lt;p&gt;The project wants you to compare merging vs. rebasing on the &lt;em&gt;same&lt;/em&gt; divergent history, so I rewound using the reflog and rebased instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; ee0c23f   &lt;span class="c"&gt;# pre-merge state, found via git reflog&lt;/span&gt;
git switch greet
git rebase master
&lt;span class="c"&gt;# conflict again — resolve, then:&lt;/span&gt;
git add lib/hello.sh
git rebase &lt;span class="nt"&gt;--continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, merging &lt;code&gt;greet&lt;/code&gt; back into &lt;code&gt;master&lt;/code&gt; was a clean fast-forward — no merge commit at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch master
git merge greet
&lt;span class="c"&gt;# Fast-forward&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fast-forward vs. merge vs. rebase, the short version:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast-forward&lt;/strong&gt;: &lt;code&gt;master&lt;/code&gt; had no new commits since &lt;code&gt;greet&lt;/code&gt; branched off, so Git just slides the &lt;code&gt;master&lt;/code&gt; pointer forward to match &lt;code&gt;greet&lt;/code&gt;. No new commit, history stays linear.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge&lt;/strong&gt;: both branches have unique commits, so Git creates a new commit with two parents, preserving exactly what happened and when. Honest history, but the graph gets messy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rebase&lt;/strong&gt;: replays your branch's commits one by one on top of the new base, producing a straight line as if you'd branched off &lt;em&gt;now&lt;/em&gt; instead of earlier. Clean history, but it rewrites commit hashes — never do this on a branch someone else is also working from.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local, Remote, and Bare Repositories
&lt;/h2&gt;

&lt;p&gt;Cloning is just copying the whole object database plus setting up a remote automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone hello cloned_hello
&lt;span class="nb"&gt;cd &lt;/span&gt;cloned_hello

git remote &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="c"&gt;# origin  /home/devuser/Desktop/work/hello (fetch)&lt;/span&gt;
&lt;span class="c"&gt;# origin  /home/devuser/Desktop/work/hello (push)&lt;/span&gt;

git branch &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="c"&gt;# * master&lt;/span&gt;
&lt;span class="c"&gt;#   remotes/origin/HEAD -&amp;gt; origin/master&lt;/span&gt;
&lt;span class="c"&gt;#   remotes/origin/greet&lt;/span&gt;
&lt;span class="c"&gt;#   remotes/origin/master&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fetch pulls down remote history without touching your working files; merge (or pull, which is both at once) actually integrates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch
git merge origin/master
&lt;span class="c"&gt;# equivalent to:&lt;/span&gt;
git pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracking a remote-only branch locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git branch &lt;span class="nt"&gt;--track&lt;/span&gt; greet origin/greet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pushing to an actual server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin https://git.example.com/devuser/hello.git
git push origin master
git push origin greet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Bare repositories
&lt;/h3&gt;

&lt;p&gt;A &lt;strong&gt;bare repo&lt;/strong&gt; has no working directory — just the raw &lt;code&gt;.git&lt;/code&gt; internals sitting at the top level. You never edit files in it directly; it exists purely as a shared exchange point that other repos push to and pull from. This is literally what GitHub, GitLab, and Gitea run on the server side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--bare&lt;/span&gt; hello hello.git
&lt;span class="nb"&gt;ls &lt;/span&gt;hello.git
&lt;span class="c"&gt;# branches config description HEAD hooks info objects packed-refs refs&lt;/span&gt;

git remote add shared ../hello.git
git add README.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docs: update README for shared repo"&lt;/span&gt;
git push shared master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pulling those shared changes into the other clone:&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="nb"&gt;cd&lt;/span&gt; ~/Desktop/work/cloned_hello
git remote add shared ../hello.git
git pull shared master

git log &lt;span class="nt"&gt;--oneline&lt;/span&gt;
&lt;span class="c"&gt;# 0a72da5 docs: update README for shared repo   &amp;lt;- now present here too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Actually Stuck
&lt;/h2&gt;

&lt;p&gt;The exercise that reframed the most for me was the &lt;code&gt;oops&lt;/code&gt; tag / reset / reflog sequence. Before this project, &lt;code&gt;git reset --hard&lt;/code&gt; felt like deleting things. Watching a "deleted" commit still show up in &lt;code&gt;git log --all&lt;/code&gt; — and only really disappearing after &lt;code&gt;reflog expire&lt;/code&gt; + &lt;code&gt;gc --prune=now&lt;/code&gt; — made it click that Git almost never destroys data on its own. It just stops pointing at it.&lt;/p&gt;

&lt;p&gt;Second was rebase vs. merge, done back to back on the identical conflict. Reading about the difference is one thing; hitting the &lt;em&gt;same&lt;/em&gt; conflict twice, once resolved with a merge commit and once resolved by replaying commits, made the tradeoff concrete instead of theoretical.&lt;/p&gt;

&lt;p&gt;If you're doing this project (or something like it): don't skip the &lt;code&gt;.git/objects/&lt;/code&gt; exploration section. It's the one part that explains &lt;em&gt;why&lt;/em&gt; everything else in Git works the way it does.&lt;/p&gt;

&lt;p&gt;Closing Thoughts&lt;br&gt;
Git has a reputation for being something you memorize commands for rather than actually understand — git add ., git commit -m "fix", git push, repeat. This project forced the opposite approach: build small, break things on purpose, and dig into why each command does what it does before moving to the next one.&lt;br&gt;
If there's one takeaway to carry forward, it's this — Git rarely deletes anything without you explicitly telling it to (and meaning it). Once that clicks, branching, resetting, and rebasing stop feeling dangerous and start feeling like tools you can actually reach for with confidence.&lt;br&gt;
If you're working through something similar, don't rush the sections that feel "boring" (looking at you, blobs and trees) — that's usually where the real understanding is hiding.&lt;br&gt;
Thanks for reading — feel free to drop questions or your own Git war stories in the comments. 🐙&lt;/p&gt;

</description>
      <category>git</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Linux Permissions, Actually Explained (Not Just Memorized)</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Mon, 13 Jul 2026 20:13:33 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/linux-permissions-actually-explained-not-just-memorized-33eb</link>
      <guid>https://dev.to/amayo_clinton/linux-permissions-actually-explained-not-just-memorized-33eb</guid>
      <description>&lt;p&gt;If you've been using Linux for more than a week, you've typed &lt;code&gt;chmod 755&lt;/code&gt; or &lt;code&gt;chmod 644&lt;/code&gt; into a terminal without really knowing what those numbers meant. You copy-pasted it from Stack Overflow, it worked, and you moved on. Fair enough — most days, that's all you need.&lt;/p&gt;

&lt;p&gt;But permissions show up everywhere: SSH key errors, deploy scripts that mysteriously fail, Docker volumes that refuse to be read, cron jobs that silently do nothing. At some point "I'll just chmod 777 it" stops being a joke and starts being a real security hole. So let's actually break this down — what the letters mean, what the numbers mean, and how they connect — so you're reasoning about permissions instead of guessing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Permissions Apply To
&lt;/h2&gt;

&lt;p&gt;Every file and directory on a Unix-like system has exactly three permission "classes" checked against it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User&lt;/strong&gt; — the account that owns the file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group&lt;/strong&gt; — a set of accounts assigned to the file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other&lt;/strong&gt; — literally everyone else on the system&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it. There's no "admin" class, no per-user access control list in the base model — just owner, group, and the rest of the world. When the kernel decides whether you're allowed to read, write, or execute something, it checks which of these three buckets you fall into, in that order, and applies the matching permission set.&lt;/p&gt;

&lt;p&gt;This is worth sitting with for a second, because it explains a lot of "why can't I access this file" confusion. If you're not the owner and you're not in the file's group, Linux doesn't care that you're a sudo-capable admin on the box — it applies the &lt;em&gt;other&lt;/em&gt; permissions, full stop (unless you escalate privileges, which is a separate mechanism entirely).&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading Symbolic Notation
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;ls -l&lt;/code&gt; in any directory and you'll see a string like &lt;code&gt;-rwxr-xr--&lt;/code&gt; sitting to the left of every filename. This is symbolic notation, and once you know the pattern, it reads like a sentence.&lt;/p&gt;

&lt;p&gt;The very first character isn't a permission at all — it's the file type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; regular file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l&lt;/code&gt; symbolic link&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;c&lt;/code&gt; character device&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b&lt;/code&gt; block device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything after that comes in groups of three, one group per class, always in the order &lt;strong&gt;user, group, other&lt;/strong&gt;. Each group has the same three-slot pattern: read, write, execute.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;r&lt;/code&gt; — permission granted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt; — permission granted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt; — permission granted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt; — permission &lt;em&gt;not&lt;/em&gt; granted, in that slot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So &lt;code&gt;-rwxr-xr--&lt;/code&gt; breaks down as: a regular file, owned by a user who can read/write/execute it, a group that can read/execute it (but not write), and everyone else who can only read it.&lt;/p&gt;

&lt;p&gt;One subtlety that trips people up: &lt;code&gt;x&lt;/code&gt; on a &lt;strong&gt;directory&lt;/strong&gt; doesn't mean "execute" in the programmatic sense — it means "you're allowed to &lt;code&gt;cd&lt;/code&gt; into it or access files inside it by name." Read on a directory just lets you list its contents (&lt;code&gt;ls&lt;/code&gt;). You genuinely need both &lt;code&gt;r&lt;/code&gt; and &lt;code&gt;x&lt;/code&gt; to browse a directory normally, which is why you'll often see &lt;code&gt;rwxr-xr-x&lt;/code&gt; on folders instead of &lt;code&gt;rw-r--r--&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Octal Notation and the Math Behind It
&lt;/h2&gt;

&lt;p&gt;Symbolic notation is readable but verbose. Octal notation compresses the same information into up to four digits, and it's what you'll actually type most of the time with &lt;code&gt;chmod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trick is that each permission has a fixed numeric weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;r&lt;/code&gt; = 4&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt; = 2&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt; = 1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the digit for a class, you just add up whichever permissions are granted. &lt;code&gt;rwx&lt;/code&gt; is &lt;code&gt;4+2+1 = 7&lt;/code&gt;. &lt;code&gt;rw-&lt;/code&gt; is &lt;code&gt;4+2 = 6&lt;/code&gt;. &lt;code&gt;r-x&lt;/code&gt; is &lt;code&gt;4+1 = 5&lt;/code&gt;. &lt;code&gt;r--&lt;/code&gt; is just &lt;code&gt;4&lt;/code&gt;. There's no ambiguity — every combination of read/write/execute maps to exactly one number from 0 to 7, because these are binary flags being summed, not arbitrary values.&lt;/p&gt;

&lt;p&gt;Do this once for each of the three classes and you get a three-digit mode. &lt;code&gt;rwxr-xr--&lt;/code&gt; becomes &lt;code&gt;7&lt;/code&gt; (user) &lt;code&gt;5&lt;/code&gt; (group) &lt;code&gt;4&lt;/code&gt; (other), or &lt;code&gt;754&lt;/code&gt;. That's genuinely the entire algorithm — there's no lookup table to memorize, just three additions.&lt;/p&gt;

&lt;p&gt;A fourth, leading digit exists too (covered below), which is why you'll sometimes see four-digit modes like &lt;code&gt;0755&lt;/code&gt; or &lt;code&gt;4755&lt;/code&gt; — the leading &lt;code&gt;0&lt;/code&gt; just means "no special bits set," and it's often omitted since &lt;code&gt;chmod 755&lt;/code&gt; and &lt;code&gt;chmod 0755&lt;/code&gt; do the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  chmod in Practice
&lt;/h2&gt;

&lt;p&gt;Once you can do the read/write/execute math in your head, &lt;code&gt;chmod&lt;/code&gt; stops being a magic incantation. Here are the combinations you'll actually reach for on a day-to-day basis:&lt;/p&gt;

&lt;p&gt;A few notes on judgment calls, not just syntax:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;755&lt;/code&gt; for anything executable&lt;/strong&gt; — scripts, binaries you built, CLI tools. Owner gets full control, everyone else can run it and see it, nobody but you can modify it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;644&lt;/code&gt; for regular files&lt;/strong&gt; — source code, configs meant to be world-readable, static assets. Nobody but the owner should be writing to most files by default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;700&lt;/code&gt; for anything private&lt;/strong&gt; — your SSH &lt;code&gt;~/.ssh&lt;/code&gt; directory, personal scripts with credentials baked in. SSH will actually refuse to use a private key if its permissions are too open, which is one of the few places Linux enforces permission hygiene for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;777&lt;/code&gt; is a code smell, not a fix.&lt;/strong&gt; If you're reaching for &lt;code&gt;chmod 777&lt;/code&gt; to make an error go away, you haven't found the actual permission problem — you've just removed the protection that would've told you what it was. It's fine on a disposable container for five minutes of debugging; it's not fine on anything that touches real data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also skip octal entirely and use symbolic (relative) syntax, which is handy when you only want to change &lt;em&gt;one&lt;/em&gt; thing without recalculating the whole mode:&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="nb"&gt;chmod &lt;/span&gt;u+x deploy.sh      &lt;span class="c"&gt;# add execute for the owner only&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;g-w config.yml     &lt;span class="c"&gt;# remove write from the group&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;&lt;span class="nv"&gt;o&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;r notes.txt      &lt;span class="c"&gt;# set other to read-only, exactly&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;a+r README.md      &lt;span class="c"&gt;# add read for everyone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;u&lt;/code&gt;, &lt;code&gt;g&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt; map to user, group, other, all — and &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;=&lt;/code&gt; add, remove, or set exactly. This form doesn't require you to know the existing permissions, which makes it safer for quick, targeted changes on files you didn't set up yourself.&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;-R&lt;/code&gt; to any &lt;code&gt;chmod&lt;/code&gt; command to apply it recursively to a directory and everything inside it — but be careful, because blanket-applying &lt;code&gt;755&lt;/code&gt; recursively will also make every &lt;em&gt;file&lt;/em&gt; executable, which is rarely what you want. For directory trees, it's usually safer to set directory and file permissions separately with &lt;code&gt;find&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;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; d &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;755 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;   &lt;span class="c"&gt;# directories: 755&lt;/span&gt;
find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-exec&lt;/span&gt; &lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="o"&gt;{}&lt;/span&gt; &lt;span class="se"&gt;\;&lt;/span&gt;   &lt;span class="c"&gt;# files: 644&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Fourth Digit: setuid, setgid, and the Sticky Bit
&lt;/h2&gt;

&lt;p&gt;Beyond the standard nine permission bits, there's a fourth, less commonly used set that changes &lt;em&gt;how&lt;/em&gt; execution or ownership behaves, rather than just who has access. These are the special permission bits, and they're where a leading digit like &lt;code&gt;4&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, or &lt;code&gt;1&lt;/code&gt; comes from in a four-digit octal mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;setuid (4000)&lt;/strong&gt; — when set on an executable, the program runs with the &lt;em&gt;file owner's&lt;/em&gt; privileges, not the privileges of whoever launched it. The classic example is &lt;code&gt;/usr/bin/passwd&lt;/code&gt;: it needs to write to &lt;code&gt;/etc/shadow&lt;/code&gt;, which regular users can't touch directly, so it's owned by root and setuid'd — letting any user change their own password without being root themselves. In symbolic notation, this shows up as an &lt;code&gt;s&lt;/code&gt; where the owner's execute bit would normally be: &lt;code&gt;rwsr-xr-x&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;setgid (2000)&lt;/strong&gt; — similar idea, but for the group. On an executable, it runs with the file's group privileges instead of the group of whoever ran it. On a &lt;em&gt;directory&lt;/em&gt;, it does something different and genuinely useful: any new file created inside inherits the directory's group automatically, instead of the creating user's default group. This is the standard trick for shared team directories where everyone needs files to end up in the same group without remembering to &lt;code&gt;chgrp&lt;/code&gt; manually every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky bit (1000)&lt;/strong&gt; — almost exclusively used on directories. It restricts deletion: even if a user has write access to a directory (and could therefore normally delete or rename anything in it), the sticky bit means they can only delete or rename files &lt;em&gt;they themselves own&lt;/em&gt;. &lt;code&gt;/tmp&lt;/code&gt; is the textbook example — it's world-writable (&lt;code&gt;777&lt;/code&gt;) so any process can drop temp files there, but the sticky bit stops one user from deleting another user's temp files. Symbolically it appears as &lt;code&gt;t&lt;/code&gt; in the other-execute slot: &lt;code&gt;rwxr-xr-t&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You combine these by adding their weights the same way you add &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;w&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;: a directory that's setgid &lt;em&gt;and&lt;/em&gt; has permissions &lt;code&gt;755&lt;/code&gt; becomes &lt;code&gt;2755&lt;/code&gt;. A world-writable sticky directory is &lt;code&gt;1777&lt;/code&gt;, which is exactly what &lt;code&gt;/tmp&lt;/code&gt; uses in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ownership: chown and chgrp
&lt;/h2&gt;

&lt;p&gt;Permissions decide &lt;em&gt;what&lt;/em&gt; a class can do; ownership decides &lt;em&gt;who's in&lt;/em&gt; the user and group classes in the first place. These usually travel together:&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="nb"&gt;chown &lt;/span&gt;jarret file.txt          &lt;span class="c"&gt;# change the owner&lt;/span&gt;
&lt;span class="nb"&gt;chgrp &lt;/span&gt;devs file.txt            &lt;span class="c"&gt;# change the group&lt;/span&gt;
&lt;span class="nb"&gt;chown &lt;/span&gt;jarret:devs file.txt     &lt;span class="c"&gt;# change both at once&lt;/span&gt;
&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; jarret:devs project/  &lt;span class="c"&gt;# recursively, for a whole directory tree&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You generally need root (or sudo) to change ownership to another user, for obvious reasons — otherwise anyone could hand their files off to someone else to dodge disk quotas or audit trails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging Real Permission Errors
&lt;/h2&gt;

&lt;p&gt;Knowing the theory is one thing — recognizing it in the wild is another. A few situations you'll run into constantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Permission denied" when running a script you just wrote.&lt;/strong&gt; Nine times out of ten, you forgot the execute bit. Creating a file with a text editor gives it &lt;code&gt;644&lt;/code&gt; by default, which has no execute permission for anyone. &lt;code&gt;chmod +x script.sh&lt;/code&gt; (shorthand for &lt;code&gt;chmod a+x&lt;/code&gt;) fixes it without you needing to work out the full octal mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH refuses your private key with "UNPROTECTED PRIVATE KEY FILE".&lt;/strong&gt; SSH deliberately checks that your private key isn't readable by group or other, because a key anyone on the system can read isn't really private. The fix is almost always &lt;code&gt;chmod 600 ~/.ssh/id_rsa&lt;/code&gt; — owner read/write, nobody else anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A web server can't read files you just deployed.&lt;/strong&gt; This is usually an ownership mismatch, not a permission mismatch — the files are owned by your deploy user, but the web server runs as &lt;code&gt;www-data&lt;/code&gt; or &lt;code&gt;nginx&lt;/code&gt;, which falls into the &lt;em&gt;other&lt;/em&gt; class and may not have read access if the mode is something tight like &lt;code&gt;600&lt;/code&gt;. The fix is either loosening the mode to &lt;code&gt;644&lt;/code&gt;/&lt;code&gt;755&lt;/code&gt; for other, or better, putting both accounts in a shared group and using &lt;code&gt;640&lt;/code&gt;/&lt;code&gt;750&lt;/code&gt; with &lt;code&gt;chgrp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cron job works when you run it manually but silently fails on schedule.&lt;/strong&gt; Cron often runs as a different user (or with a stripped-down environment) than your interactive shell. If the script or the files it touches aren't readable/executable by whatever user cron runs as, it fails quietly with no terminal to show you the error — check &lt;code&gt;/var/log/syslog&lt;/code&gt; or &lt;code&gt;journalctl -u cron&lt;/code&gt; for the actual permission denial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker volume mounted from the host has the wrong permissions inside the container.&lt;/strong&gt; Containers frequently run processes as a different UID than your host user, even though the filesystem UID numbers are shared. A file that's &lt;code&gt;644&lt;/code&gt; and owned by UID 1000 on your host might be unreadable to a container process running as UID 999. This one isn't solved by &lt;code&gt;chmod&lt;/code&gt; alone — you typically need to align the UID the container runs as with the UID that owns the mounted files.&lt;/p&gt;

&lt;p&gt;In every one of these cases, the fix falls out naturally once you ask the right question: &lt;em&gt;which class (user/group/other) is the process actually running as, and does that class have the permission it needs?&lt;/em&gt; That's the entire debugging methodology — everything above is just that question applied to a specific tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;Permissions on Linux really come down to three ideas stacked on top of each other: three classes (user, group, other), three abilities per class (read, write, execute), and one compact way to write all nine bits as three digits by adding 4, 2, and 1. Everything else — setuid, setgid, sticky bits, symbolic shorthand — is a small extension on top of that same base logic, not a separate system to memorize.&lt;/p&gt;

&lt;p&gt;Next time you type &lt;code&gt;chmod 644&lt;/code&gt; without thinking about it, you'll know exactly what those two digits are doing — and more importantly, you'll be able to work out the &lt;em&gt;right&lt;/em&gt; number for a situation you haven't seen a Stack Overflow answer for yet.&lt;/p&gt;




&lt;p&gt;That's the whole model — three classes, three abilities, one bit of addition, and a few special cases layered on top. If you've got a permissions war story of your own (a &lt;code&gt;777&lt;/code&gt; you regret, a setgid trick that saved a shared directory, a Docker UID mismatch that took way too long to track down), drop it in the comments — I'd genuinely like to hear it.&lt;/p&gt;

&lt;p&gt;And if this was useful, a follow or a ❤️ helps more people find it. Thanks for reading!&lt;/p&gt;

</description>
      <category>linux</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Beyond the Lone Cheetah: Architecture Patterns for Multi-Agent Prides in Real-World Ecosystems</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Tue, 07 Jul 2026 23:18:02 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/beyond-the-lone-cheetah-architecture-patterns-for-multi-agent-prides-in-real-world-ecosystems-4f6b</link>
      <guid>https://dev.to/amayo_clinton/beyond-the-lone-cheetah-architecture-patterns-for-multi-agent-prides-in-real-world-ecosystems-4f6b</guid>
      <description>&lt;p&gt;Most engineers treat large language models like erratic, omniscient interns. They throw loose, natural-language prose into an API endpoint, something vague like "screen these loan applications for risk," and then act surprised when the model hallucinates a Western corporate SaaS model, drifts past compliance rules, or reproduces demographic bias baked into its training data.&lt;/p&gt;

&lt;p&gt;In emerging financial ecosystems across Africa, from Nairobi's tech corridor to Kampala's e-commerce networks, the cost of these engineering failures isn't theoretical. An automation layer that misreads a cash flow pattern doesn't just dent a vanity metric. It can lock legitimate users out of credit or misjudge liquidity in a community savings structure. So the practical response is to stop treating an LLM as a conversational oracle and start treating it as a component that speaks in token probabilities and needs the same boundaries you'd put around any other untrusted input source.&lt;/p&gt;

&lt;p&gt;What follows are the patterns I've found useful for moving from isolated, fragile automations toward coordinated, auditable multi-agent systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Calibrating autonomy with a bounded-authority gate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an agent executes real actions, like triggering a microfinance payment or sending a notification, unbounded execution power is an invitation to trouble. Take a simple failure case: a customer-service agent handling refunds with no hard limits. Without constraints, it can be talked into approving an invalid refund just because the input says something like "I'll escalate this on social media."&lt;/p&gt;

&lt;p&gt;The fix is a layered gate that checks role scope first, then a hard authority ceiling, and routes anything outside those bounds to a human, with a kill switch that overrides everything else regardless of where in the flow it fires:&lt;/p&gt;

&lt;p&gt;Incoming request&lt;br&gt;
     |&lt;br&gt;
     v&lt;br&gt;
Role check (does this fall within the agent's defined scope?)&lt;br&gt;
     |&lt;br&gt;
     v&lt;br&gt;
Authority check (is the value within the agent's approval ceiling?)&lt;br&gt;
     |&lt;br&gt;
  within bounds --&amp;gt; execute autonomously&lt;br&gt;
  outside bounds --&amp;gt; freeze and escalate to a human&lt;/p&gt;

&lt;p&gt;In code, the kill switch is checked first, before role or authority, because it's a global override and should short-circuit everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoanTriageAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tier-1 Loan Triage Specialist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_authority_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15000&lt;/span&gt;  &lt;span class="c1"&gt;# KES approval ceiling
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_context&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transaction_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;risk_flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AGENT_SYSTEM_ACTIVE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FALSE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ESCALATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Kill switch triggered&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;transaction_amount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_authority_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ESCALATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Exceeded authority ceiling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debt collector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;children_under_5&lt;/span&gt;&lt;span class="sh"&gt;"&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="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ESCALATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Vulnerable-demographic flag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;risk_flags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DENY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Multiple concurrent risk flags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVE_AUTONOMOUSLY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point isn't the specific thresholds, it's that the ceiling and the escalation path are explicit, testable, and sit outside the model's own judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auditing for bias before deployment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Models inherit the historical patterns in their training data, and in micro-lending or savings-cooperative contexts that can show up as a systematic gap between how the model treats informal-sector applicants (market vendors, seasonal traders) versus formally employed ones with comparable repayment capacity, simply because the training corpus underrepresents informal income patterns like agricultural liquidity cycles.&lt;/p&gt;

&lt;p&gt;Before shipping a model into that kind of decision path, it's worth running it through a structured bias check rather than eyeballing a handful of outputs:&lt;/p&gt;

&lt;p&gt;Check the balance of formal-versus-informal-sector examples in the training or fine-tuning data.&lt;br&gt;
Confirm the model has explicit vocabulary for informal work (a market trader or a cooperative member shouldn't be an out-of-distribution case).&lt;br&gt;
Track approval-rate disparities across applicant segments on a rolling basis in production.&lt;br&gt;
Run counterfactual swaps: hold cash flow constant, change only the sector label, and see if the output changes.&lt;br&gt;
Set a hard threshold, for example, a fallback to manual review if the disparity between segments crosses a defined limit.&lt;/p&gt;

&lt;p&gt;Wiring this kind of check into CI, rather than treating fairness as a one-time review, is what turns it from an aspiration into something you can actually catch in a pull request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coordinating state across a chain of agents&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multi-agent systems fail quietly when each agent works in isolation and drops context between steps. A three-agent pipeline, a Scout that handles the initial conversation, a Guardian that scores risk, and a Reviewer that packages the case for a human, only works if all three share a single state contract rather than passing loosely-typed messages between themselves.&lt;/p&gt;

&lt;p&gt;Scout agent (intake) --&amp;gt; Guardian agent (risk scoring) --&amp;gt; Reviewer agent (human handoff)&lt;br&gt;
                    \                              /&lt;br&gt;
                     shared context: balances, harvest windows, applicant metadata&lt;/p&gt;

&lt;p&gt;Concretely: the Scout agent picks up an unstructured message like "no money for school fees right now" and converts it into a structured handoff:&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;"handoff_event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FINANCIAL_STRESS_SIGNAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&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;"child_age_metrics"&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="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target_district"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kakamega"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"next_harvest_window"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"October/November"&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;The Guardian agent takes that payload and scores it against seasonal repayment capacity rather than a static monthly-income model, since income variance tied to a harvest window is a very different risk shape than a salaried applicant's would be. If the resulting score lands in a mid-confidence band, it attaches context flags and passes the case along rather than deciding alone.&lt;/p&gt;

&lt;p&gt;The Reviewer agent doesn't have write access to the ledger. Its only job is to turn the shared state into a short brief a human can act on quickly, something like: applicant is a maize farmer in Kakamega with income peaking in October and November, has three school-age children, is requesting funds for fees, has no active risk flags, and the system's suggestion is to match repayment to the November harvest and note her as a candidate for drought-index insurance.&lt;/p&gt;

&lt;p&gt;That last step matters as much as the scoring: a human should be able to approve or reject the case from that brief alone, without digging back through the raw conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closing the loop with production telemetry&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Static agent configurations degrade as real-world conditions shift, so it's worth building a lightweight feedback loop that watches production metrics and proposes, rather than silently applies, adjustments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review_weekly_telemetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;csat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CSAT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;escalations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extract_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalation_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;csat&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;proposal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;component&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scout_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;change&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Adjust term-start calendar handling&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reasoning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CSAT dropped among a specific cohort; investigate calendar misalignment.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;send_for_human_approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#prod-ops-approvals&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proposal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Optimization proposal generated and sent for review.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the phrasing here is intentionally cautious: without a labeled comparison group, "CSAT dropped among rural users because of long rains" is a hypothesis to investigate, not a conclusion to log as fact. The loop's job is to surface the anomaly and route it to a person, not to auto-diagnose the cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The underlying principle&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of this is about clever prose engineering. It's regular software engineering applied to a component that happens to be probabilistic: bound its authority explicitly, test it for bias the same way you'd test for a regression, give agents in a pipeline a single shared contract instead of ad hoc handoffs, and treat any proposed change to production behavior as something a human signs off on, not something the system does to itself quietly.&lt;/p&gt;

&lt;p&gt;How are you handling authority limits and state handoffs in your own agent pipelines? I'd like to hear what's worked and what hasn't.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>architecture</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>The AUR Supply Chain Attack: When Trust Becomes the Vulnerability</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Thu, 18 Jun 2026 07:02:46 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/the-aur-supply-chain-attack-when-trust-becomes-the-vulnerability-dah</link>
      <guid>https://dev.to/amayo_clinton/the-aur-supply-chain-attack-when-trust-becomes-the-vulnerability-dah</guid>
      <description>&lt;p&gt;This Month, the Arch Linux community discovered that more than four hundred packages in the Arch User Repository had been quietly hijacked. The attackers did not exploit a bug in any software. They exploited something harder to patch: the trust that builds up around a package's name and history.&lt;/p&gt;

&lt;p&gt;What happened&lt;/p&gt;

&lt;p&gt;The Arch User Repository, commonly known as the AUR, is a community driven collection of build scripts maintained separately from Arch's official repositories. Anyone can adopt an abandoned package whose original maintainer has stopped updating it. That openness is exactly what this campaign relied on.&lt;/p&gt;

&lt;p&gt;Researchers at Sonatype, who named the operation Atomic Arch, found that attackers were systematically adopting orphaned AUR packages, the ones whose maintainers had simply walked away. Once in control, they edited the PKGBUILD or install scripts to quietly run a command during the build process: npm install atomic-lockfile. That single line pulled in a malicious npm package alongside a couple of legitimate ones included for cover. The npm package carried a preinstall hook that dropped a bundled Linux binary named deps, and simply building the AUR package was enough to trigger it.&lt;/p&gt;

&lt;p&gt;To make the changes look legitimate, the attackers also spoofed git commit metadata so it appeared the edits came from a long standing maintainer. An Arch Linux Trusted User later confirmed that maintainer's account had never actually been compromised. The packages kept their familiar names and commit histories. Only the build instructions underneath had changed.&lt;/p&gt;

&lt;p&gt;What the malware actually does&lt;/p&gt;

&lt;p&gt;Independent researcher Whanos reverse engineered the deps payload and identified it as a Rust based credential stealer built for developer machines and build systems. Once running, it can pull cookies, session tokens, and local storage from Chromium based browsers including Chrome, Edge, and Brave. It also lifts session data from Electron apps such as Slack, Discord, and Microsoft Teams. On top of that it harvests GitHub, npm, and HashiCorp Vault tokens, OpenAI and ChatGPT bearer credentials, SSH keys and known hosts files, shell history, Docker and Podman credentials, and VPN profiles.&lt;/p&gt;

&lt;p&gt;The stolen data is exfiltrated over HTTP to the file sharing service temp.sh, while command and control traffic runs through a Tor onion service via a local loopback proxy. For persistence, the malware installs a systemd service set to restart automatically. If it landed with root access, it copies itself into /var/lib/ and installs a system level unit. Without root, it falls back to the user's home directory and a per user systemd unit.&lt;/p&gt;

&lt;p&gt;There is also an optional eBPF rootkit component, though early reporting overstated its role. It does not grant any privilege escalation and only activates if the binary already has root and the right capability. When it does load, it uses pinned BPF maps to hide the malware's processes, names, and socket inodes from standard monitoring tools, and it actively blocks debugger attachment. A second, unanalyzed binary tied to a fake monero wallet GUI package was also found staged alongside the stealer, suspected to be a cryptominer.&lt;/p&gt;

&lt;p&gt;This combination matters for remediation. A package manager can remove the files it installed, but it cannot prove a machine is clean once a rootkit capable payload has had the chance to run.&lt;/p&gt;

&lt;p&gt;Scope and a second wave&lt;/p&gt;

&lt;p&gt;Sonatype's initial report counted just over twenty compromised packages. Within a day, community trackers and the Arch mailing list had cataloged more than four hundred, with one list built by scanning the AUR git mirror directly landing around four hundred and eight, and later consolidated counts climbing even higher. Notably, the malicious atomic-lockfile npm package itself had only around one hundred and thirty four weekly downloads before it was pulled from the registry, meaning the AUR build process, not direct npm installation, was the real distribution channel.&lt;/p&gt;

&lt;p&gt;A second, separate wave surfaced using the command bun install js-digest, traced to a different set of npm accounts that community researchers link to the same publisher behind atomic-lockfile. Its payload is a distinct binary by hash, also confirmed malicious. The full extent of this second wave is still being measured, since grep based searches of the AUR mirror return different totals depending on how much commit churn is included, but it is not a minor footnote to the first wave. Anyone checking their system should look for both atomic-lockfile and js-digest.&lt;/p&gt;

&lt;p&gt;What to do if you use the AUR&lt;/p&gt;

&lt;p&gt;Arch maintainers are resetting the malicious commits and banning the implicated accounts, and they are asking the community to keep reporting suspicious packages through the mailing list thread. For anyone who has built or updated an AUR package since June 11, the published list of affected packages should be treated as incomplete rather than authoritative.&lt;/p&gt;

&lt;p&gt;Practical steps worth taking include checking recently built or updated AUR packages against community maintained detection lists and scripts, and searching build history and caches for references to npm install atomic-lockfile, bun install js-digest, or the payload path src/hooks/deps. If any flagged package was built on your system, the safest assumption is that the host is credential compromised. That means rotating browser sessions, SSH keys, GitHub and npm tokens, Slack, Teams, and Discord sessions, Vault tokens, Docker and Podman credentials, and any cloud access keys.&lt;/p&gt;

&lt;p&gt;It is also worth hunting for persistence mechanisms directly, including unrecognized systemd services both at the system level and under a user's own systemd config directory, unexpected files under /var/lib/, and the presence of pinned BPF maps named hidden_pids, hidden_names, and hidden_inodes under /sys/fs/bpf/. Reviewing outbound network connections for Tor traffic or uploads to temp.sh is another useful signal. If a compromised package was built as root, the recommendation is to treat the rootkit as present and reinstall the system from trusted media, since there is no reliable way to confirm a clean state otherwise.&lt;/p&gt;

&lt;p&gt;For detection purposes, the primary payload's SHA two fifty six hash is 6144d433f8a0316869877b5f834c801251bbb936e5f1577c5680878c7443c98b, and a fuller set of indicators, including the onion command and control address, has been published in the ioctl.fail writeup.&lt;/p&gt;

&lt;p&gt;The bigger lesson&lt;/p&gt;

&lt;p&gt;This is not the first time an abandoned package has been weaponized this way. A similar tactic was used against an abandoned PDF viewer package back in 2018. What makes the 2026 version notable is the scale and the layered payload, combining credential theft with an optional kernel level hiding mechanism rather than relying on typosquatting or tricking users into installing the wrong thing entirely.&lt;/p&gt;

&lt;p&gt;Sonatype is tracking the campaign as Sonatype 2026 003775 with a CVSS score of 8.7, though no CVE has been assigned since this is fundamentally a trust and process failure rather than a software vulnerability. The real takeaway for anyone using community package repositories is that a name and a commit history are not proof of safety. A package that has recently changed hands, or one that has suddenly become active again after long dormancy, deserves the same scrutiny you would give to software from a complete stranger. Reading the PKGBUILD and any install hooks before building is no longer optional diligence. It is the only line of defense the trust model has left.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>security</category>
    </item>
    <item>
      <title>When AI Gets It Wrong: The Hidden Security Risk of Hallucinations in Cybersecurity</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Sun, 17 May 2026 13:18:42 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/when-ai-gets-it-wrong-the-hidden-security-risk-of-hallucinations-in-cybersecurity-2gel</link>
      <guid>https://dev.to/amayo_clinton/when-ai-gets-it-wrong-the-hidden-security-risk-of-hallucinations-in-cybersecurity-2gel</guid>
      <description>&lt;p&gt;AI systems are getting better at detecting threats — but what happens when they confidently give you the wrong answer?&lt;/p&gt;

&lt;p&gt;Cover image suggestion: A glitching neural network visualization or a cracked padlock with circuit board texture.&lt;/p&gt;

&lt;p&gt;AI is rapidly being embedded into security operations — threat detection, incident response, log analysis, vulnerability triage. The efficiency gains are real. But there's a category of risk that doesn't get nearly enough attention: AI hallucination, and specifically what happens when it occurs inside a security context.&lt;/p&gt;

&lt;p&gt;This isn't a hypothetical. It's a pattern already emerging in production environments, and the consequences can range from alert fatigue to catastrophic data loss&lt;/p&gt;

&lt;p&gt;What Is AI Hallucination in a Security Context?&lt;/p&gt;

&lt;p&gt;AI hallucination refers to when a model generates outputs that are confident, fluent, and factually wrong. Unlike a crash or an obvious error, hallucinations look correct. The model doesn't flag uncertainty — it just answers.&lt;/p&gt;

&lt;p&gt;In low-stakes contexts (drafting an email, summarizing a document), a hallucination is annoying. In a security context, it can be catastrophic.&lt;/p&gt;

&lt;p&gt;Here's why that matters: security teams are trained to trust their tooling. When a SIEM or AI-assisted SOC platform flags an alert, analysts act on it. When an AI recommends a remediation step, engineers often execute it. The trust relationship that makes AI useful in security operations is the same relationship that makes hallucinations so dangerous there.&lt;/p&gt;

&lt;p&gt;Three Ways AI Hallucinations Become Security Incidents&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;False Positives That Erode Trust&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When an AI system repeatedly generates false positive alerts — flagging benign behavior as malicious — teams naturally adapt. They begin treating alerts with suspicion. They start skipping review steps. Over time, the entire alerting pipeline loses credibility.&lt;/p&gt;

&lt;p&gt;This is the alert fatigue problem at scale. And the insidious part is that it doesn't require the AI to fail dramatically. It just needs to be wrong often enough that humans stop believing it — at which point a real threat will look exactly like every other false alarm.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;False Negatives That Create Blind Spots&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The flip side is equally dangerous. AI systems trained on historical threat data can fail to recognize novel attack patterns — zero-days, new malware families, unusual lateral movement techniques. A model that has never seen a particular attack vector may simply not flag it.&lt;/p&gt;

&lt;p&gt;The problem is amplified when teams have calibrated their response processes around AI-assisted triage. If the AI doesn't raise an alert, the threat may never receive human attention at all.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Incorrect Remediation Guidance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is arguably the most dangerous failure mode, because it happens after trust has already been established.&lt;/p&gt;

&lt;p&gt;Imagine: an AI correctly identifies a threat. The analyst trusts it. They then ask the AI what to do about it — and the AI confidently recommends deleting sensitive files, modifying critical system configurations, or disabling firewall rules.&lt;/p&gt;

&lt;p&gt;If those actions are executed — especially via privileged accounts — the result can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identity-based attack exposure from weakened access controls&lt;/li&gt;
&lt;li&gt;Lateral movement enabled by disabled network segmentation&lt;/li&gt;
&lt;li&gt;Irreversible data loss from confident-sounding but incorrect deletion commands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The attack surface doesn't just remain open. It widens. And the team may not realize the AI was wrong until the damage is already done.&lt;/p&gt;

&lt;p&gt;Why This Keeps Happening&lt;/p&gt;

&lt;p&gt;AI hallucinations in security contexts aren't a single-cause problem. They emerge from a combination of factors:&lt;/p&gt;

&lt;p&gt;Training data quality. Models learn from historical data. If that data contains outdated threat signatures, biased datasets, or inaccurate records, those flaws surface in the model's outputs. As AI-generated content becomes more prevalent online, there's a growing risk of future models being trained on content produced by earlier hallucinating models — a compounding phenomenon sometimes called model collapse.&lt;/p&gt;

&lt;p&gt;Prompt quality. Vague inputs give models more room to fill gaps with assumptions. A security analyst asking "what should I do about this?" gets a very different (and often less reliable) output than one asking "given this specific log output and these network conditions, what are three targeted remediation steps with rollback options?"&lt;/p&gt;

&lt;p&gt;Over-trust in model confidence. AI models produce confident-sounding outputs regardless of accuracy. There's no built-in signal for "I'm not sure about this." Teams that don't build human review into their workflows have no circuit breaker.&lt;/p&gt;

&lt;p&gt;What Teams Can Actually Do About It&lt;/p&gt;

&lt;p&gt;These risks aren't reasons to avoid AI in security operations. They're reasons to deploy it thoughtfully. Here's what effective governance looks like in practice.&lt;/p&gt;

&lt;p&gt;Require Human Review Before Privileged Action&lt;/p&gt;

&lt;p&gt;AI-generated recommendations should not automatically trigger sensitive operations — infrastructure changes, access modifications, incident response actions — without human verification first. This applies even when the AI seems right. Models produce equally confident outputs whether they're correct or not, so the review step cannot be conditional on "something seems off."&lt;/p&gt;

&lt;p&gt;Build this into your runbooks and your tooling, not just your policies.&lt;/p&gt;

&lt;p&gt;Treat Training Data as a Security Asset&lt;/p&gt;

&lt;p&gt;Your AI is only as reliable as what it learned from. Regularly audit the data used to train or ground your AI systems. Remove outdated threat signatures, biased datasets, and known inaccurate records. As AI-generated content proliferates across the internet, continuous data governance isn't optional — it's a prerequisite for reliable model outputs.&lt;/p&gt;

&lt;p&gt;Apply Least-Privilege to AI Systems, Not Just Humans&lt;/p&gt;

&lt;p&gt;If an AI system recommends deleting a file, it should not have permission to delete that file. Apply the same least-privilege principles to AI-driven systems that you apply to human accounts: read where reading is enough, no write access where writes aren't required, no delete access anywhere it isn't explicitly necessary.&lt;/p&gt;

&lt;p&gt;This way, even if a hallucinated recommendation makes it through review, the blast radius is bounded by access controls.&lt;/p&gt;

&lt;p&gt;Invest in Prompt Engineering for Security Teams&lt;/p&gt;

&lt;p&gt;Output quality is heavily shaped by input quality. Security teams that interact directly with AI systems need to understand how to write prompts that are specific, verifiable, and actionable — not because prompt engineering is magic, but because vague inputs systematically increase hallucination risk.&lt;/p&gt;

&lt;p&gt;Train your analysts not just to use AI tools, but to evaluate AI outputs critically. The mental model to instill: the AI is a fast, confident junior analyst. Always review their work.&lt;/p&gt;

&lt;p&gt;Put Identity Security at the Center of AI Governance&lt;/p&gt;

&lt;p&gt;Most AI hallucinations become security incidents  at the moment of action — when an AI system or a human trusts an incorrect output enough to execute something. This is fundamentally an access problem.&lt;/p&gt;

&lt;p&gt;The question to ask in your architecture review: if the AI says the worst possible thing and someone believes it, what's the most damage that can happen? Work backward from that to design your access controls, audit logging, and review requirements.&lt;/p&gt;

&lt;p&gt;Security architectures that enforce least-privilege for both human and non-human identities (NHIs), monitor privileged activity, and require verification before sensitive actions are taken give organizations meaningful protection even when AI outputs are wrong.&lt;/p&gt;




&lt;p&gt;The Bottom Line&lt;/p&gt;

&lt;p&gt;AI in security operations is a force multiplier — for better and for worse. When it works, it catches threats faster than human analysts can. When it hallucinates, it can create the conditions for a breach while appearing to be managing one.&lt;/p&gt;

&lt;p&gt;The answer isn't to distrust AI. It's to be precise about where trust is warranted and what safeguards exist when that trust is misplaced.&lt;/p&gt;

&lt;p&gt;Human review requirements, least-privilege enforcement, data governance, and prompt engineering discipline aren't obstacles to AI adoption. They're what makes AI adoption survivable.&lt;/p&gt;




&lt;p&gt;Have you seen AI hallucinations cause real problems in a security context? Share your experience in the comments — this is a space where practitioner knowledge matters a lot.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Amayo Clinton</dc:creator>
      <pubDate>Sun, 26 Apr 2026 12:46:42 +0000</pubDate>
      <link>https://dev.to/amayo_clinton/-330i</link>
      <guid>https://dev.to/amayo_clinton/-330i</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/oketch/your-guide-into-the-development-world-a-roadmap-for-absolute-beginners-5fh6" class="crayons-story__hidden-navigation-link"&gt;Your Guide Into the Development World: A Roadmap for Absolute Beginners&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/oketch" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879643%2Fd07e2208-cddd-41f0-8252-4a9c59fcbc20.jpg" alt="oketch profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/oketch" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Dishon Oketch
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Dishon Oketch
                
              
              &lt;div id="story-author-preview-content-3552839" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/oketch" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879643%2Fd07e2208-cddd-41f0-8252-4a9c59fcbc20.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Dishon Oketch&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/oketch/your-guide-into-the-development-world-a-roadmap-for-absolute-beginners-5fh6" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 26&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/oketch/your-guide-into-the-development-world-a-roadmap-for-absolute-beginners-5fh6" id="article-link-3552839"&gt;
          Your Guide Into the Development World: A Roadmap for Absolute Beginners
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/codenewbie"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;codenewbie&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/programming"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;programming&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/beginners"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;beginners&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/oketch/your-guide-into-the-development-world-a-roadmap-for-absolute-beginners-5fh6" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/oketch/your-guide-into-the-development-world-a-roadmap-for-absolute-beginners-5fh6#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
  </channel>
</rss>
