<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>Paul Hammant&apos;s blog</title>
 <link href="https://paulhammant.com/atom.xml" rel="self"/>
 <link href="https://paulhammant.com"/>
 <rights>Copyright (c) 2002 - 2015, Paul James Hammant</rights>
 <updated>2026-09-30T11:36:15+00:00</updated>
 <id>https://paulhammant.com</id>
 <author>
   <name>Paul Hammant</name>
   <email>paul@hammant.org</email>
 </author>

 
 <entry>
   <title>BTRON, OLE, OpenDoc and the Web: Why Dumb Won</title>
   <link href="https://paulhammant.com/2026/09/27/btron-ole-opendoc-and-why-the-dumb-web-won/"/>
   <updated>2026-09-27T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/09/27/btron-ole-opendoc-and-why-the-dumb-web-won</id>
   <content type="html">&lt;p&gt;Note: By “dumb” I don’t mean badly designed. I mean something more specific: the architecture with the weakest 
internal semantics had the strongest boundaries.&lt;/p&gt;

&lt;p&gt;Japan’s TRON project bubbled up again recently - an &lt;a href=&quot;https://www.xda-developers.com/japan-tried-build-operating-system-entire-world-us-government-intervened/&quot;&gt;XDA piece by Adam Conway&lt;/a&gt;
and an &lt;a href=&quot;https://www.osnews.com/story/145872/japan-tried-to-build-an-operating-system-for-the-entire-world-tron/&quot;&gt;OSNews write-up by Thom Holwerda&lt;/a&gt; with a good comment thread. BTRON, the desktop branch, is
the interesting one. It tried to replace “files owned by applications” with a document model:
typed parts, handlers per part type, and persistent system-managed links between parts. A
document holding writing, a table and a figure would invoke a handler for each, with each handler
drawing inside the parent document’s window. Its filesystem was an arbitrary directed graph, not a
tree. That was late 1980s (it only began in 1984).&lt;/p&gt;

&lt;p&gt;Meanwhile ITRON, the embedded sibling, quietly became one of the most deployed operating systems in
the world - in cameras, engine control units and phones - because it was small, deterministic and
royalty-free. That’s the “dumb won” story in miniature, inside TRON itself. Maybe dumb-won is too 
pithy and &lt;em&gt;the architecture with the weakest internal semantics had the strongest boundaries&lt;/em&gt; is more 
accurate.&lt;/p&gt;

&lt;p&gt;Thom’s pet theory is that the application-first model won because it is better at wealth
extraction. Commenter ssokolow disagreed: compound documents were a fine idea while documents
mostly left the building as paper or PDF, but once the Internet arrived, making the recipient
acquire &lt;em&gt;every handler for every embedded part&lt;/em&gt; became prohibitive. Another commenter, metaning,
remembered OpenDoc files sent to a service bureau, which then had to own the same OpenDoc
components you did before they could RIP your job to an imagesetter.&lt;/p&gt;

&lt;p&gt;I think ssokolow is closer, and I went back and forth with ChatGPT to put it in a table.&lt;/p&gt;

&lt;h2 id=&quot;four-answers-to-what-comes-after-files--applications&quot;&gt;Four answers to “what comes after files + applications?”&lt;/h2&gt;

&lt;p&gt;Between roughly 1984 and 1997 the industry tried repeatedly to answer that question. Four
answers stand out:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;BTRON&lt;/strong&gt; - hypermedia + compound documents at the OS level&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OLE&lt;/strong&gt; - compound documents retrofitted onto the conventional Windows desktop&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OpenDoc&lt;/strong&gt; - documents as federations of small, interchangeable parts (Apple/IBM et al)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Web&lt;/strong&gt; - hypermedia stripped down enough to become universal&lt;/li&gt;
&lt;/ol&gt;

&lt;table class=&quot;table table-striped table-bordered&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Aspect&lt;/th&gt;
      &lt;th&gt;BTRON&lt;/th&gt;
      &lt;th&gt;Microsoft OLE&lt;/th&gt;
      &lt;th&gt;OpenDoc&lt;/th&gt;
      &lt;th&gt;Web&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Peak innovation period&lt;/td&gt;
      &lt;td&gt;1985-1990&lt;/td&gt;
      &lt;td&gt;1990-1996&lt;/td&gt;
      &lt;td&gt;1992-1997&lt;/td&gt;
      &lt;td&gt;1989-1996&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Core vision&lt;/td&gt;
      &lt;td&gt;Computer as a structured hypermedia/document environment&lt;/td&gt;
      &lt;td&gt;Desktop as compound documents assembled from application components&lt;/td&gt;
      &lt;td&gt;Documents assembled from independent interchangeable parts&lt;/td&gt;
      &lt;td&gt;A universal distributed hypertext information space&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fundamental unit&lt;/td&gt;
      &lt;td&gt;Typed document part&lt;/td&gt;
      &lt;td&gt;COM/OLE object&lt;/td&gt;
      &lt;td&gt;OpenDoc part&lt;/td&gt;
      &lt;td&gt;Resource / document&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Applications are …&lt;/td&gt;
      &lt;td&gt;Handlers for part types&lt;/td&gt;
      &lt;td&gt;COM servers / containers&lt;/td&gt;
      &lt;td&gt;Part editors&lt;/td&gt;
      &lt;td&gt;Browser + servers + helper apps&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Linking&lt;/td&gt;
      &lt;td&gt;System-managed typed links&lt;/td&gt;
      &lt;td&gt;OLE links, monikers&lt;/td&gt;
      &lt;td&gt;Part relationships&lt;/td&gt;
      &lt;td&gt;Simple one-way hyperlinks&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Links survive move/rename?&lt;/td&gt;
      &lt;td&gt;Designed to&lt;/td&gt;
      &lt;td&gt;Potentially, via monikers&lt;/td&gt;
      &lt;td&gt;Within its framework&lt;/td&gt;
      &lt;td&gt;No - link rot&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Requires a new OS worldview?&lt;/td&gt;
      &lt;td&gt;Largely yes&lt;/td&gt;
      &lt;td&gt;No - retrofit onto Windows&lt;/td&gt;
      &lt;td&gt;No - over existing OSes&lt;/td&gt;
      &lt;td&gt;No - runs above almost anything&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Network-native?&lt;/td&gt;
      &lt;td&gt;Not BTRON’s killer property&lt;/td&gt;
      &lt;td&gt;No (DCOM later)&lt;/td&gt;
      &lt;td&gt;Not fundamentally&lt;/td&gt;
      &lt;td&gt;Absolutely&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ease of copying content from the canvas&lt;/td&gt;
      &lt;td&gt;High in principle, within BTRON tooling&lt;/td&gt;
      &lt;td&gt;High for ordinary representations; lower for preserving live-object semantics&lt;/td&gt;
      &lt;td&gt;High by design&lt;/td&gt;
      &lt;td&gt;High — browser provides common selection/copy for ordinary document content&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ease of pasting elsewhere, incl. lesser technologies&lt;/td&gt;
      &lt;td&gt;Medium - TAD interchange, but semantics lost leaving the ecosystem&lt;/td&gt;
      &lt;td&gt;High to medium - clipboard formats degrade, but rich objects need their server&lt;/td&gt;
      &lt;td&gt;Medium-high - depends on the receiver&lt;/td&gt;
      &lt;td&gt;Extremely high - HTML, text, image or just the URL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Navigation master control&lt;/td&gt;
      &lt;td&gt;Medium - navigation integral, but no universal user-owned stack&lt;/td&gt;
      &lt;td&gt;Low - belongs to each container/app&lt;/td&gt;
      &lt;td&gt;Low-medium - container coordinates parts&lt;/td&gt;
      &lt;td&gt;Extremely high - Back / Forward / Reload / Stop + location bar sit above the page&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Historical outcome&lt;/td&gt;
      &lt;td&gt;Desktop branch stayed niche (ITRON thrived embedded)&lt;/td&gt;
      &lt;td&gt;Deeply embedded in Windows/Office&lt;/td&gt;
      &lt;td&gt;Terminated 1997&lt;/td&gt;
      &lt;td&gt;Ate the world&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The dates are approximate, and mean when each was introducing its distinctive ideas rather than
when it peaked commercially.&lt;/p&gt;

&lt;p&gt;The first eight rows are the conventional comparison, and on those the Web looks primitive. It’s the
dumbest of the four. The three rows I asked to be added just before “historical outcome” are
where it wins, and I think they are the rows that matter.&lt;/p&gt;

&lt;p&gt;Then also “high - browser supplies selection/copy for every page” re copy/paste wasn’t there from the outset of the
web, had a peak, and has declined quite a bit in the last decade. Canvas-rendered applications, images of text, 
custom controls, DRM-ish interfaces, CSS selection suppression, etc. can defeat normal selection. And I have to 
say I really hate that defeating of one of the true genius aspects of the web.&lt;/p&gt;

&lt;h2 id=&quot;graceful-degradation-outward&quot;&gt;Graceful degradation outward&lt;/h2&gt;

&lt;p&gt;The richer systems optimized for movement of content &lt;em&gt;within themselves&lt;/em&gt;: from a BTRON part to
another BTRON part, from an OLE server to an OLE container, from one OpenDoc part editor to
another. That’s interoperability between equally sophisticated peers.&lt;/p&gt;

&lt;p&gt;To be fair, BTRON did think about degradation &lt;em&gt;inside&lt;/em&gt; the system: applications could skip TAD
data types they didn’t support. But Conway’s article has a telling detail: if no handler exists for
a part, or the handler fails to draw, the system strikes a diagonal line through the region where
the content should have been. That’s the compound-document failure mode drawn literally - a box
you can’t see into, which is exactly what ssokolow’s recipient without the right handlers gets.&lt;/p&gt;

&lt;p&gt;The Web’s artifacts instead degrade gracefully when they leave:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CONTENT:
interactive page
  -&amp;gt; HTML / structured fragment
    -&amp;gt; formatted text + images
      -&amp;gt; plain text

REFERENCE:
interactive page
  -&amp;gt; URL
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;^ The Web has both graceful representation degradation and cheap referential escape&lt;/p&gt;

&lt;p&gt;At each step you throw away semantics and still have something useful. The destination needn’t
understand the source. An email client, a Slack message, a Word doc, a text file, or a sticky note
on a monitor will each take &lt;em&gt;some&lt;/em&gt; level of that. That’s precisely ssokolow’s point about
handlers: the compound-document systems required the receiver to be as capable as the sender,
and on the open Internet that’s rarely the case.&lt;/p&gt;

&lt;p&gt;A better name for the row might be &lt;strong&gt;“graceful degradation when copied/pasted outward”&lt;/strong&gt;. The
question is how easily can information escape the system without the destination knowing
anything about it?&lt;/p&gt;

&lt;h2 id=&quot;navigation-master-control&quot;&gt;Navigation master control&lt;/h2&gt;

&lt;p&gt;This is the one I find most under-appreciated. OLE says the container/application controls your
experience. OpenDoc says the compound document coordinates your parts. BTRON says the information
environment manages your graph. The browser says:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;- Back   -&amp;gt; Forward   Reload   Stop   [ Location ]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;… and the page doesn’t get a vote (fundamentally the page doesn’t own those controls). 
A terrible page and a brilliant page get the same
navigation machinery, owned by the user, sitting outside the document. Back is a generic
undo-like operation over navigation that requires &lt;em&gt;no cooperation from the content author&lt;/em&gt;.
The location bar gives Web resources - and well-designed application states - a name the user 
can copy independently of their contents.&lt;/p&gt;

&lt;p&gt;Single-page apps eventually had to learn to cooperate with that model via the History API: if 
application state changes aren’t reflected in browser history, Back stops meaning what users expect.&lt;/p&gt;

&lt;p&gt;Green-screen techs as represented via IBM’s “common user interface” definitions from the late 1980s, 
had an F12 for back-one-screen (not page) and F3 for back-out-completely (exit) didn’t make the jump to the web, 
even if F5 for refresh did. Well, less and less today.&lt;/p&gt;

&lt;h2 id=&quot;what-about-lotus-notes&quot;&gt;What about Lotus Notes?&lt;/h2&gt;

&lt;p&gt;Notes was a love-it-or-hate-it thing, and it’s a fifth answer from the same window. Ray Ozzie’s
Iris Associates started in late 1984 and Notes shipped in late 1989. The fundamental unit was the
document - rich text with embedded objects and attachments - and an “application” was really just
a database design (forms, views, some scripting) wrapped around documents.&lt;/p&gt;

&lt;p&gt;Its links had one important BTRON-like property: they were based on persistent identities rather than merely filesystem paths. 
Doclinks keyed on a replica ID plus the document’s unique ID (UNID),
so they &lt;em&gt;could&lt;/em&gt; survive replication and server moves where the Web’s rot. And it had a strength none of the
four above had: replication. Offline-first documents that synced between servers and laptops,
years before anyone said “local-first”.&lt;/p&gt;

&lt;p&gt;But on the three rows that matter, it looked like OLE and OpenDoc. Rich Notes content degraded unevenly 
outside the Notes ecosystem; preserving Notes-specific behavior required Notes-aware software, 
and navigation belonged to the Notes client - until Release 5
added browser-like Back/Forward and bookmarks, which was Notes imitating the browser. The “Domino” version
(1996 or so) then served Notes databases over HTTP as HTML. Domino eventually gave Notes databases a 
Web escape route: HTTP/HTML could expose their contents without requiring the Notes client.&lt;/p&gt;

&lt;p&gt;Ozzie kept pushing the replication idea after Notes - Groove (2000-2005) for peer-to-peer
shared workspaces, then Live Mesh (2008) at Microsoft. It was the browser-based,
URL-addressed takes on the same idea - Google Docs, then Microsoft’s own cloud Office - that won.&lt;/p&gt;

&lt;p&gt;Notes’ data model did survive, though: Damien Katz described CouchDB (JSON docs, map/reduce views,
replication) as “Lotus Notes built from the ground up for the web,” and PouchDB (~2012-13 (?)) put the offline-first
replicating client in the browser. The easy built-in UI didn’t survive - as I noted in
&lt;a href=&quot;/2013/01/30/lotus-notes-vs-couchdb/&quot;&gt;Lotus Notes vs CouchDB&lt;/a&gt; back in 2013 - the Web’s own UI
stack took that job.&lt;/p&gt;

&lt;p&gt;The linked-parts half of the idea is back too. Conway notes that Roam, Logseq and Obsidian have
spent the last decade rediscovering BTRON’s model of linked, transcludable blocks - mostly as
local-first files on disk.&lt;/p&gt;

&lt;h2 id=&quot;denny-called-this-in-1990---in-reverse&quot;&gt;Denny called this in 1990 - in reverse&lt;/h2&gt;

&lt;p&gt;Back in 2013 I wrote about &lt;a href=&quot;/2013/03/28/interface-builders-alternative-lisp-timeline/&quot;&gt;Interface Builder’s alternative Lisp timeline&lt;/a&gt;,
and Dennison “Denny” Bollay told me about “Dynamic Documents”, built for the CIA after Xerox PARC’s NoteCards.
It had unidirectional, bidirectional, contextual and searchable links, stored out-of-band in an
object database, and linking arbitrary portions of documents from word processors, drawing
programs and databases. When the CIA handed them an HTML spec, they and PARC concluded it was
“dumb from almost every angle”: every document had to be rewritten in HTML, links couldn’t be
contextual, links were one-way with no dead-link checking, it was completely static, and “even
HTTP was stupid”.&lt;/p&gt;

&lt;p&gt;“Obviously we misjudged…”, he added.&lt;/p&gt;

&lt;p&gt;Most of those criticisms accurately identify capabilities the early Web omitted compared with richer hypertext systems, 
and in the same post I noted that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Action!&lt;/code&gt; itself had no
forward/back button and no URL concept. Denny’s list is really a list of the Web’s &lt;em&gt;trade-offs&lt;/em&gt;,
and the three rows above are what those trade-offs bought. Converging on a small set of simple, 
openly specified representations made graceful degradation much easier. 
Links being the same for all users is what makes
a URL something you can paste into an email. Static documents with a browser in charge are what
let the user, not the author, own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;back&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;so-wealth-extraction&quot;&gt;So, wealth extraction?&lt;/h2&gt;

&lt;p&gt;I don’t think Thom is wrong that application-first suits vendors - look at how many SaaS products
break copy/paste, deep-linking or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Back&lt;/code&gt; on purpose today. But BTRON, OLE and OpenDoc didn’t lose to
greed. They lost to something simpler that was easier to &lt;em&gt;leave&lt;/em&gt;. If someone wants to try
document-centric computing again, ssokolow’s advice holds: solve the dependency problem first. And
I’d add: keep the Back button outside the document, and make sure every part can collapse to plain
text and a URL.&lt;/p&gt;

&lt;h2 id=&quot;where-do-phones-fit&quot;&gt;Where do phones fit?&lt;/h2&gt;

&lt;p&gt;That’s where the burning hot race is, so iOS and Android deserve a look against the three rows that
matter. I’ve kept them out of the big table as their peak years (roughly 2007-2014) would break the
1984-1997 story:&lt;/p&gt;

&lt;table class=&quot;table table-striped table-bordered&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Concept&lt;/th&gt;
      &lt;th&gt;iOS&lt;/th&gt;
      &lt;th&gt;Android&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Fundamental unit&lt;/td&gt;
      &lt;td&gt;Sandboxed app&lt;/td&gt;
      &lt;td&gt;App, but composed of Activities wired together by Intents&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Content extractability&lt;/td&gt;
      &lt;td&gt;Low-medium - many apps block text selection or draw their own UI&lt;/td&gt;
      &lt;td&gt;Same&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Graceful degradation outside the ecosystem&lt;/td&gt;
      &lt;td&gt;Via the share sheet - transfers typed representations—URLs, text, images, files, etc .. to another app&lt;/td&gt;
      &lt;td&gt;Via share Intents - typed representations including text, URLs, images and files.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;User-owned navigation layer&lt;/td&gt;
      &lt;td&gt;Weak - no system Back; a per-app nav bar, the edge-swipe convention, and the “◀ Previous App” breadcrumb&lt;/td&gt;
      &lt;td&gt;Medium - a system Back exists, apps can intercept it, and “predictive back” is slowly pulling it back under OS control&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;A few things stand out:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;BTRON’s bet paid off - upside down.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Phones hid the filesystem (iOS until the Files app in 2017)
and imposed a whole new platform worldview, on a non-x86 CPU no less. Nobody had to be talked out
of the desktop, because the phone was a new device category. But what won inside that new worldview
was the app silo - the opposite of documents composed of parts. This is where Thom’s wealth
extraction theory has its strongest evidence: a 30% store cut gives the platform an economic incentive to
keep content inside apps.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Android Intents are the closest mainstream thing to BTRON’s handlers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Well, maybe closest: “I have an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image/jpeg&lt;/code&gt;, who can EDIT it?” and the OS offers a chooser. 
The handler model survived, just at whole-app granularity rather than parts-within-a-document.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Android popularized the verb × typed-data model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACTION_VIEW&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACTION_EDIT&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACTION_SEND&lt;/code&gt;
against a MIME type, with an “Open with … Always / Just once” chooser, made handler selection
everyday UI for the first time. Most flows are VIEW or SEND though - EDIT rarely surfaces.&lt;/p&gt;

&lt;p&gt;The idea was old. Prior art:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Unix mailcap (metamail, Bellcore, ~1991 (?); RFC 1524, 1993 (?)) - a view command per MIME type,
plus optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit=&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compose=&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print=&lt;/code&gt; fields. Possibly the first view/edit split per MIME
type (?). Mutt and pine users configured it; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;edit=&lt;/code&gt; was rarely filled in.&lt;/li&gt;
  &lt;li&gt;OLE 1.0 (1990-91 (?)) - per-class verbs for embedded objects. A sound object’s primary verb was
“Play” and “Edit” was secondary. The one place desktop users visibly met the idea.&lt;/li&gt;
  &lt;li&gt;Windows registry (3.1’s REG.DAT 1992 (?), routine by Windows 95) - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shell\&amp;lt;verb&amp;gt;\command&lt;/code&gt; per
file type, keyed on extension rather than MIME. Right-click “Edit” on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.bat&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.reg&lt;/code&gt; opened
Notepad instead of running or importing it; on XP, “Edit” on an image opened Paint.&lt;/li&gt;
  &lt;li&gt;Mac OS X Launch Services - apps declare themselves Viewer or Editor per document type. 
Architecturally explicit, but largely invisible in ordinary UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In ordinary desktop use, users overwhelmingly interacted with one cell of the matrix: Open. And
that cell became a battleground - media players and browsers hijacking associations on install,
“make X your default?” nags, and eventually Windows 10 hash-protecting defaults so apps couldn’t
grab them silently. Nobody fought over the Edit slot. Vendors fought fiercely over the one slot that 
decides whose app you live in. Whatever the motivation, that’s at least compatible with Thom’s 
wealth-extraction argument: the economically valuable position wasn’t “editor for this type”; it 
was “default place where this type opens.”&lt;/p&gt;

&lt;p&gt;The Web nevertheless supplies the most universal 
escape hatch: when an app has a public Web representation, sharing a URL exports a reference that 
virtually any destination can preserve, even if it understands none of the app’s internal data model. 
Deep links and universal links are apps adopting URLs after the fact. The
graceful-degradation row still holds on phones - it’s just the Web providing it, not the platform.&lt;/p&gt;

&lt;p&gt;And in closing, &lt;strong&gt;the hot race today is a fight over navigation master control.&lt;/strong&gt; Apple’s App Intents (since iOS 16) and
Google’s equivalents on Android let apps expose typed actions that an OS-level assistant or agent can
invoke across apps. That’s a layer above the apps that the user - or their agent - drives: browser
chrome again. Architecturally, it rhymes with BTRON/OpenDoc composition: applications expose typed capabilities that a 
higher-level orchestrator can combine, with apps as verb-handlers stitched
together by an orchestrator. The open question is the same one as 1993: who owns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Back&lt;/code&gt;, and who owns
the layer above the content? Platform vendors are increasingly putting an OS-level assistant/orchestration layer above apps.
The Web put that layer in the user agent.&lt;/p&gt;

&lt;h2 id=&quot;files--applications-again&quot;&gt;Files + applications again&lt;/h2&gt;

&lt;p&gt;They don’t go away just yet. I enjoy using my Chromebook and Google is an almost entirely online company, but they
still have dirs and files and apps that would operate on them for ChromeOS. I’m 70% in that world day
to day. Google are rolling out AluminiumOS presently so I get to find out how much my preferred OS
changes in the transition, I guess.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Charting AI Today: What It Leaves Behind</title>
   <link href="https://paulhammant.com/2026/07/26/charting-ai-today/"/>
   <updated>2026-07-26T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/07/26/charting-ai-today</id>
   <content type="html">&lt;p&gt;A few days ago I wrote about &lt;a href=&quot;/2026/07/20/build-time-vs-run-time-ai/&quot;&gt;build-time AI vs run-time AI&lt;/a&gt;,
after the Thoughtworks
&lt;a href=&quot;https://www.thoughtworks.com/about-us/events/the-future-of-software-development-europe-2026&quot;&gt;Future of Software Development Retreat&lt;/a&gt;
in Engelberg - three days in June, convened by Martin Fowler and Thoughtworks, following an
earlier gathering in Utah in February. Two write-ups are out now: Martin’s
&lt;a href=&quot;https://martinfowler.com/fragments/2026-07-21.html&quot;&gt;closing fragment&lt;/a&gt; and the official
&lt;a href=&quot;https://www.thoughtworks.com/content/dam/thoughtworks/documents/report/tw_future_of_software_engineering_europe_2026.pdf&quot;&gt;Thoughtworks report&lt;/a&gt;.
Five headline findings, of which the first is the one that matters here: &lt;em&gt;code generation is no
longer the bottleneck - verification is.&lt;/em&gt; Agents can produce code, specs, tests and
infrastructure faster than any team can trust it. The trick is - how to match the generated code’s tests to the generated code’s prod source.&lt;/p&gt;

&lt;p&gt;My post was a 3×2 table. I’ve been thinking about it since, and the table wasn’t really getting accross what I wanted it to. Too many
things sat awkwardly across the line, and the interesting distinctions were happening &lt;em&gt;inside&lt;/em&gt;
the cells rather than between them.&lt;/p&gt;

&lt;p&gt;So Claude and I made a chart instead. It’s &lt;a href=&quot;/code/ai/chart.html&quot;&gt;here&lt;/a&gt;, and it has toggles so you can play with it. 15 years ago 
I was using AngularJS for interactivty within SVG, and today I don’t ask Claude what it picked. Indeed Claude would have said yes 
to any of 50 JS/TS techs.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/code/ai/chart.html&quot;&gt;&lt;img src=&quot;/images/ai-chart.png&quot; alt=&quot;AI systems plotted by artifact left behind against execution control&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-axis-that-took-the-longest&quot;&gt;The axis that took the longest&lt;/h2&gt;

&lt;p&gt;I started with “artefact surface”: versioned artifacts on the left, operational state on the right.
Two halves, four quadrants, done. My biases toward source control systems were clear - which I what I wanted.&lt;/p&gt;

&lt;p&gt;It wasn’t two halves though. Pure chat has no versioned artifact and doesn’t touch operational state.
Media synthesis has a library - durable, real, yours - that nothing downstream reads. And
agents like OpenClaw don’t have a &lt;em&gt;surface&lt;/em&gt; so much as an absence of one: whatever the machine
can reach by whatever means … which is why I’ve not tried one of the many claws.&lt;/p&gt;

&lt;p&gt;The version I settled on runs through six waypoints:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat Log → Versioned Artifacts → Media Library → Change Specific → Change Anything → Become&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The ordering isn’t reach, and it isn’t persistence. It’s what’s left behind that someone can
inspect. A transcript you can read. A commit that gets reviewed before it counts. Assets in a
library. Records other biz systems depend on. Traces wherever the thing happened to reach. And then
ultimately “Become” rightmost.&lt;/p&gt;

&lt;h2 id=&quot;persistence-rises-then-does-something-strange&quot;&gt;Persistence rises, then does something strange&lt;/h2&gt;

&lt;p&gt;If you follow the waypoints expecting durability to climb steadily, you’ll be wrong twice.&lt;/p&gt;

&lt;p&gt;A chat log is less durable than a generated video file. And a media library is arguably &lt;em&gt;more&lt;/em&gt; durable
than a commit while being far less connected - nothing downstream reads it, which is why it sits
where it does rather than further right. Durability and consequence aren’t the same axis, and
conflating them is how you end up thinking Sora and alike are more consequential than Dependabot.&lt;/p&gt;

&lt;h2 id=&quot;what-become-means&quot;&gt;What “Become” means&lt;/h2&gt;

&lt;p&gt;The last waypoint is the one I keep coming back to. Someone built an operating system that was
all AI - no code for the file editor, no code for the browser, no code for the spreadsheet.
Just a model behaving editor-ly on demand. I’ll add the link when I can find it again.&lt;/p&gt;

&lt;p&gt;My first instinct was that nothing survives there. That’s wrong. If the AI-generated word
processor saves a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.docx&lt;/code&gt; file, the file is on a filesystem, as real as one written by actual code.&lt;/p&gt;

&lt;p&gt;Two artifacts are in play and “Become” splits them:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The output:  your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.docx&lt;/code&gt; file. Survives fine.&lt;/li&gt;
  &lt;li&gt;The producer:  the word processor itself. Doesn’t exist as reviewable code, and may not be
the same word processor next time you open the document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the sharper edge: with real code, save either worked or threw an exception. Here, “Saved!”
is generated text. The file might be there. Might be elsewhere. Might not exist. You can’t read
the save path to check, because there is no save path. You’re verifying from outside, every time.&lt;/p&gt;

&lt;p&gt;Sam Ruby, &lt;a href=&quot;https://intertwingly.net/blog/2026/07/01/What-Survived-Contact.html&quot;&gt;on what survived contact&lt;/a&gt;:
“rigor doesn’t vanish when the agent writes the code - it migrates. Upstream into specifications,
down into test suites treated as first-class artifacts.” His sharper claim is that the oracle -
the objective, and how you evaluate whether you met it - is the asset that can’t be delegated.
Implementation can be generated and thrown away; the oracle can’t.&lt;/p&gt;

&lt;p&gt;Which gives me a cleaner way to say what Become is. Not the absence of code - the absence of an
oracle. Nothing independent left to check the output against.&lt;/p&gt;

&lt;p&gt;That’s not the absence of an artifact. It’s the absence of any &lt;em&gt;guarantee&lt;/em&gt; about one, which is
worse. Perhaos thats absence you’d notice.  See “side story” at the bottom about how the industry 
does historicaly like things that work but bobody can explain how precesiley - with the risk 
being “if it breaks, can it be fixed and how long will that take and what guarantees.”&lt;/p&gt;

&lt;h2 id=&quot;the-overlays&quot;&gt;The overlays&lt;/h2&gt;

&lt;p&gt;The chart has three toggle rows, and the reason they’re overlays rather than axes is that none
of them are positional. They cut across.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Containment&lt;/strong&gt; - Augmented, Harnessed, Unharnessed. Augmented is the bottom of the chart, and
it’s mostly classic products with a model bolted into an existing deterministic frame. Zapier
didn’t get displaced by LLMs; it became a distribution channel for them. The model fills a slot;
the workflow around it stays developer-defined. That’s the deployment pattern most organisations
actually reach for, and it’s the least discussed.&lt;/p&gt;

&lt;p&gt;I’m using “harness” in roughly Thoughtworks’ sense - the scaffolding round an agent - though I
care less about how well it’s built than whether it exists at all, and whether anything checks
the output before it counts. That’s a looser test than theirs, which is why the Harnessed lasso
picks up commercial CRM and support agents where the vendor built the rig rather than you. The
rig is still there. You just didn’t assemble it, and can’t see much of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it goes wrong&lt;/strong&gt; - the four MTTD/MTTR regimes. Mean time to &lt;em&gt;detect&lt;/em&gt; and mean time to
&lt;em&gt;repair&lt;/em&gt; are independent, and people conflate them constantly. Fast/fast is CI linters and
autocomplete: wrong is visible immediately and costs seconds. Fast/slow is a Zapier job that
mangled 10,000 records overnight - you know by morning, and unwinding takes a week.&lt;/p&gt;

&lt;p&gt;Slow/slow is where it gets uncomfortable. Data enrichment writing a plausible summary into a CRM
field. Nobody notices for months. By then humans have read it and trusted it, it’s been exported
into reports, and there’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git revert&lt;/code&gt; for “this record has been quietly wrong since March.”&lt;/p&gt;

&lt;p&gt;Note that MTTR correlates with the x-axis - versioned artifacts are revertable by construction -
but MTTD doesn’t correlate with anything on the chart. Detection time is a property of how the
output gets consumed, not of what the system is. That’s why it needed its own overlay.&lt;/p&gt;

&lt;p&gt;This is the retreat’s “verification is the bottleneck” finding wearing different clothes. If
verification is what’s scarce, then MTTD is just the measure of how long unverified output sits
around being trusted. The chart’s slow-detect region is where verification isn’t happening at
all - not because it’s hard, but because nobody scheduled it.&lt;/p&gt;

&lt;p&gt;The Thoughtworks report gets to a recommendation I’d endorse from a different direction:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Adopt a risk-tiered autonomy model per system or component (cobot-style human oversight vs.
dark-factory-style full automation), explicitly based on risk, reversibility and blast
radius - do not apply a single autonomy policy uniformly across a portfolio.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reversibility and blast radius are precisely what the x-axis and the MTTx overlay are measuring.
If you want a risk-tiered autonomy model, you need a way to see which tier a given system is
actually in - and “it’s agentic” doesn’t tell you. Dependabot and OpenClaw are both agents.
They’re nowhere near each other on any dimension that matters for setting a policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human veto&lt;/strong&gt; - before it counts, after the fact, or not at all.&lt;/p&gt;

&lt;p&gt;I got this one wrong first time. I had the whole chat column as “human-free,” reasoning that
there’s no PR, no approval workflow, no review artifact. But chat has the &lt;em&gt;tightest&lt;/em&gt; veto on the
chart: you read every token before anything happens, and rejection costs one prompt. I’d
confused the absence of a formal review process with the absence of oversight. They’re not the
same thing, and the informal one is often stronger.&lt;/p&gt;

&lt;p&gt;Kief Morris, &lt;a href=&quot;https://infrastructure-as-code.com/posts/fose-july-2026.html&quot;&gt;writing up the same retreat&lt;/a&gt;,
reduces the whole thing to: “how much do we let an agent decide, and how do we stay confident in
what it does?” His ops teams keep a narrow remit - “diagnose, yes; decide, no.” That’s a veto
boundary drawn &lt;em&gt;inside&lt;/em&gt; a single system, which my chart can’t show; one dot per product doesn’t
have the resolution. He also found line-by-line code review had stopped working as a guardrail,
with rigor migrating upstream to acceptance criteria and downstream to automated checks. Worth
holding against my “before it counts” lasso: the gate is there, but it may not be carrying the
weight we assume it is.&lt;/p&gt;

&lt;p&gt;Ian Cooper’s &lt;a href=&quot;https://ian-cooper.writeas.com/coding-agents-and-driving-in-gears&quot;&gt;gears&lt;/a&gt; make the
same point from another angle: he drives in high gear for boilerplate and drops to low gear when
certainty falls or the blast radius grows - auth, payments. So it isn’t just that the veto
boundary sits inside a system rather than around it. It moves, hour to hour, with the same tool
in the same hands. My chart gives each dot one veto state and holds it there.&lt;/p&gt;

&lt;p&gt;Kief and I were at the same three-day event. Showing him the open source thing I’ve been building
in exactly his space was in my top five must-dos for the trip. I flew home without having done it.
Forty sessions (albeit multi-track) and plenty of hallway time, and the one conversation I’d
specifically planned was the one I missed.&lt;/p&gt;

&lt;p&gt;Only two dots are genuinely veto-free, both at the right-hand end, and they fail differently.
OpenClaw’s defects get found late and can’t be cleanly undone. The AI-native applications
one is worse in an odd way - there’s no defect to point at, because there’s no code to diff
against intent. You just sense that something is off - “uncanny valley” in the modern age.&lt;/p&gt;

&lt;p&gt;Abby Bangser &lt;a href=&quot;https://www.syntasso.io/post/ai-is-changing-software-engineering-the-fundamentals-still-matter&quot;&gt;makes a point I hadn’t considered&lt;/a&gt;:
“removing humans from the loop also removes organisational and product learning.” The veto isn’t
only a correctness mechanism. It’s where people find out what the system actually does - which is
the apprenticeship problem arriving by a side door.&lt;/p&gt;

&lt;h2 id=&quot;caveats&quot;&gt;Caveats&lt;/h2&gt;

&lt;p&gt;Dot placement is editorial, not measured. I’d defend the clusters and the general diagonal;
I would not defend any individual coordinate to two decimal places. The chart is a thinking
tool, not a dataset.&lt;/p&gt;

&lt;p&gt;The Media Library island property is a fact about today’s integrations, not about the
technology. The moment someone wires generated assets into a publishing pipeline, that dot
slides right. Integrations by another name.&lt;/p&gt;

&lt;p&gt;And the whole right-hand end is thinly evidenced - one shipping product and one experiment.
That’s the nature of drawing a chart about where things are going rather than where they’ve
been.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/code/ai/chart.html&quot;&gt;Have a play with it.&lt;/a&gt; The overlays combine - Augmented plus slow/slow detect
is the combination I find most interesting, and it isn’t the one anyone is writing threads about.&lt;/p&gt;

&lt;h2 id=&quot;others-who-wrote-it-up&quot;&gt;Others who wrote it up&lt;/h2&gt;

&lt;p&gt;Worth your time, and I’ve only borrowed from some of them above:
&lt;a href=&quot;https://www.ivettordog.com/blog/2026-07-05-future-of-software-engineering-retreat-reflections&quot;&gt;Ivett Ördög&lt;/a&gt;
on whether agents should maintain codebases like engineers or regenerate them like compilers -
the question my x-axis is really about;
&lt;a href=&quot;https://davidwhitney.co.uk/blog/2026/07/20/computers_finally_care_about_code/&quot;&gt;David Whitney&lt;/a&gt; on
why “almost always working isn’t enough”, and code mattering more rather than less;
&lt;a href=&quot;https://ocytko.net/posts/fose-2026-reflections/&quot;&gt;Bartosz Ocytko&lt;/a&gt; on software factories and the
handoff points where humans still sit;
&lt;a href=&quot;https://overwatering.org/blog/2026/07/notes-from-fose-europe/&quot;&gt;Giles Edwards-Alexander&lt;/a&gt; on
“Optimisers vs Learners” as two organisational shapes, and on learning being the constraint now
rather than delivery;
&lt;a href=&quot;https://www.linkedin.com/pulse/software-engineering-having-raft-moment-andrew-harmel-law-f6mle/&quot;&gt;Andrew Harmel Law&lt;/a&gt;
asking which of our practices were rafts we can put down.&lt;/p&gt;

&lt;h2 id=&quot;footnote-a-reversibility-im-not-measuring&quot;&gt;Footnote: a reversibility I’m not measuring&lt;/h2&gt;

&lt;p&gt;Mathias Verraes &lt;a href=&quot;https://verraes.net/2026/07/software-design-in-the-agentic-age/&quot;&gt;argues for keeping the ability&lt;/a&gt;
to “revert from agentic code generation to human engineering.” That’s a reversibility my chart
doesn’t measure - not undoing a change, but undoing a way of working. The x-axis says nothing
about whether you can walk back from a waypoint once you’re standing on it.&lt;/p&gt;

&lt;h2 id=&quot;side-story&quot;&gt;Side Story&lt;/h2&gt;

&lt;p&gt;In the mid-1990s, at the University of Sussex, researcher Adrian Thompson used a genetic algorithm 
to repeatedly reconfigure a Xilinx FPGA until it evolved a circuit that could distinguish between 
two audio tones. Instead of producing a neat digital design, evolution exploited subtle analogue 
properties of the physical silicon timing delays, capacitance and other quirks to create a 
solution that worked but was largely incomprehensible to its creators and often depended on the 
characteristics of that specific chip. The result was a striking demonstration that evolutionary 
search can discover highly effective solutions beyond human intuition, but it was never adopted 
for telecoms or other safety-critical industries because engineers could neither fully explain 
nor reliably verify its behaviour across different devices, temperatures and operating 
conditions. In the end, its greatest strength finding unconventional solutions unconstrained 
by human assumptions was also the reason it could not satisfy industries that require predictable, 
reproducible and certifiable designs. Are we back in that risk place now?&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Build-time AI vs Run-time AI</title>
   <link href="https://paulhammant.com/2026/07/20/build-time-vs-run-time-ai/"/>
   <updated>2026-07-20T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/07/20/build-time-vs-run-time-ai</id>
   <content type="html">&lt;p&gt;I was at a ThoughtWorks “The Future of Software Engineering” event in Switzerland some weeks ago. What a braintrust. Lots of 
conversations about AI generally, agentic AI in particular and harness engineering.&lt;/p&gt;

&lt;p&gt;Here’s a distinction I’ve been mulling for a while and keep reaching for in conversations about
AI systems in the enterprise: &lt;strong&gt;build-time AI&lt;/strong&gt; versus &lt;strong&gt;run-time AI&lt;/strong&gt;. One is artifact-oriented,
the other is execution-oriented.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Mode&lt;/th&gt;
      &lt;th&gt;Primary artifacts&lt;/th&gt;
      &lt;th&gt;Versioned?&lt;/th&gt;
      &lt;th&gt;Typical outputs&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Build-time AI&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Source files, configs, prompts, AGENTS.md, tests, policies, IaC&lt;/td&gt;
      &lt;td&gt;Yes (Git)&lt;/td&gt;
      &lt;td&gt;Commits, PRs, CI/CD, deployments&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Run-time AI&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Live state, requests, conversations, workflows&lt;/td&gt;
      &lt;td&gt;Usually no&lt;/td&gt;
      &lt;td&gt;Database rows, tickets, CRM updates, emails, transactions, generated media&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Run-time AI isn’t only transactional, either. Generation of movies, songs, images, and
slide-shows is run-time too. Those outputs are files, which makes them look artifact-like, but
they’re &lt;em&gt;products&lt;/em&gt; of execution, not engineering artifacts. Say a generated slide-show doesn’t
specify the behavior of any system, and nobody’s putting it through code review as it might be going 
straight into GoogleDocs or Office 365. That’s the real
test for which side of the line something sits on: build-time artifacts specify (later) behavior;
run-time outputs are the consequences of wished-for behaviors.&lt;/p&gt;

&lt;p&gt;Cross that with the conversational -&amp;gt; workflow -&amp;gt; agentic ladder that has become the common way of
classifying AI systems, and you get a tidy 3×2:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;AI type&lt;/th&gt;
      &lt;th&gt;Build-time&lt;/th&gt;
      &lt;th&gt;Run-time&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Conversational&lt;/td&gt;
      &lt;td&gt;Prompt engineering: templates, system prompts&lt;/td&gt;
      &lt;td&gt;A single chat session&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Workflow&lt;/td&gt;
      &lt;td&gt;Workflow engineering: workflow definitions, tool configuration&lt;/td&gt;
      &lt;td&gt;Executing the predefined process&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agentic&lt;/td&gt;
      &lt;td&gt;Harness engineering: code, prompts, tools, memory policies, guardrails&lt;/td&gt;
      &lt;td&gt;The agent reasoning, planning, acting, updating systems&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;harness-engineering-is-a-build-time-discipline&quot;&gt;Harness engineering is a build-time discipline&lt;/h1&gt;

&lt;p&gt;The interesting consequence: harness engineering is predominantly a build-time discipline, even
though it exists entirely to influence run-time behavior.&lt;/p&gt;

&lt;p&gt;The build-time harness looks like source code, because it is source code:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AGENTS.md
tools.yaml
memory_policy.py
planner.py
evals/
guardrails/
prompts/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Well, it is today. It gets committed to Git, reviewed in pull requests, and deployed via CI/CD - the full software
delivery treatment.&lt;/p&gt;

&lt;p&gt;Run-time is a different world entirely. Steps in a workflow:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Customer asks for a refund&lt;/li&gt;
  &lt;li&gt;Agent plans&lt;/li&gt;
  &lt;li&gt;Looks up CRM&lt;/li&gt;
  &lt;li&gt;Issues refund&lt;/li&gt;
  &lt;li&gt;(Audit log written)&lt;/li&gt;
  &lt;li&gt;(Database updated)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last two are in parentheses because the agent isn’t doing them directly - the existing
application or service (AI made or not) it invoked does those as a matter of course.&lt;/p&gt;

&lt;p&gt;No Git commit occurs during that interaction. The agent changed the state of several systems of
record, but it didn’t change &lt;em&gt;itself&lt;/em&gt;.&lt;/p&gt;

&lt;h1 id=&quot;an-orthogonal-axis-not-a-maturity-stage&quot;&gt;An orthogonal axis, not a maturity stage&lt;/h1&gt;

&lt;p&gt;It’s tempting to present build-time vs run-time as another rung on the maturity ladder, after
conversational, workflow, and agentic. I’d resist that. It’s an orthogonal axis. There are two
independent questions to ask of any AI system:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;What kind of AI system is this? - Conversational → workflow → agentic.&lt;/li&gt;
  &lt;li&gt;Where does the “engineering” happen? -  Build-time artifacts ↔ run-time execution.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every cell in the 3×2 above is a legitimate place to be, with the non-agentic ones included in that claim. A
prompt-engineered conversational system with its templates under version control is not “less
mature” than an agentic one. Indeed, it may be exactly the right amount of machinery for the job.&lt;/p&gt;

&lt;h1 id=&quot;what-a-harness-actually-is&quot;&gt;What a “harness” actually is&lt;/h1&gt;

&lt;p&gt;This framing also gives “harness” a satisfyingly software-engineering flavor. Edward Mangini’s
recent &lt;a href=&quot;https://www.emangini.com/blog/2026/harness_engineering&quot;&gt;Harness Engineering: The Devil is in Your
Details&lt;/a&gt; defines a harness very broadly -
essentially “everything except the model.”. He credits others (Trivedy &amp;amp; Birgitta Böckeler) for that. I think there’s a cleaner separation: the harness is 
the &lt;strong&gt;version-controlled, deployable specification of
agent behavior&lt;/strong&gt;. The live conversation, the database state, and the external side effects are not
the harness - they are the consequences of the harness executing.&lt;/p&gt;

&lt;p&gt;That gives harnesses a lifecycle human software engineers will recognize:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;**author -&amp;gt; review -&amp;gt; commit -&amp;gt; deploy -&amp;gt; execute -&amp;gt; observe -&amp;gt; revise&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything before “execute” is build-time. “Observe” is where run-time telemetry feeds back into
the next build-time iteration. Which is to say: harness engineering is just engineering, and all
the disciplines we already have for source code - code review, trunk-based development,
continuous delivery, configuration as code - apply to it directly.&lt;/p&gt;

&lt;p&gt;Well of course, build-time vs run-time isn’t common language in the AI field, so I’ll keep on trying 
to update my mental model.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Google-style DAG build systems (with Aether Build)</title>
   <link href="https://paulhammant.com/2026/07/02/google-style-dag-build-systems/"/>
   <updated>2026-07-02T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/07/02/google-style-dag-build-systems</id>
   <content type="html">&lt;p&gt;For years I’ve contrasted &lt;strong&gt;depth-first recursive&lt;/strong&gt; build technologies (Maven, Gradle, MSBuild) with
&lt;strong&gt;directed acyclic graph&lt;/strong&gt; (DAG) ones (Blaze/Bazel, Buck, Nx). I keep a small simulation repo,
&lt;a href=&quot;https://github.com/paul-hammant/google-monorepo-sim&quot;&gt;google-monorepo-sim&lt;/a&gt;, to make the difference
concrete rather than hand-wavy. This entry walks through it.&lt;/p&gt;

&lt;p&gt;The sim used to be a pile of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.compile.sh&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.sh&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dist.sh&lt;/code&gt; bash scripts with a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared-build-scripts/&lt;/code&gt; folder of common logic and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.buildStepsDoneLastExecution&lt;/code&gt; file for tracking
what had already run. That’s all gone now. The DAG side is built with
&lt;a href=&quot;https://github.com/aether-lang-org/aeb&quot;&gt;Aether Build&lt;/a&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt;), a small polyglot build system
written in &lt;a href=&quot;https://github.com/aether-lang-org/aether&quot;&gt;Aether&lt;/a&gt;. It’s still crude and teachable in a way that compares to
Bazel/Buck/Nx.&lt;/p&gt;

&lt;h1 id=&quot;multi-module-build-systems-per-language&quot;&gt;Multi-module build systems per language&lt;/h1&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Language&lt;/th&gt;
      &lt;th&gt;Depth-First Recursive&lt;/th&gt;
      &lt;th&gt;Directed Acyclic Graphs (leaf-first)&lt;/th&gt;
      &lt;th&gt;Other / Community Tools&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Python&lt;/td&gt;
      &lt;td&gt;setuptools, Poetry, SCons&lt;/td&gt;
      &lt;td&gt;Pants&lt;/td&gt;
      &lt;td&gt;Hatch, PDM&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;C++&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;CMake, Ninja, Bazel, Buck&lt;/td&gt;
      &lt;td&gt;Meson, Tup&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Java&lt;/td&gt;
      &lt;td&gt;Maven, Gradle, Ant&lt;/td&gt;
      &lt;td&gt;Bazel, Buck&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;C&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Make, CMake, Bazel&lt;/td&gt;
      &lt;td&gt;Meson, Ninja, Autotools&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;C#&lt;/td&gt;
      &lt;td&gt;MSBuild, Cake, NAnt&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Fake (F#), dotnet CLI&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JavaScript&lt;/td&gt;
      &lt;td&gt;npm/yarn&lt;/td&gt;
      &lt;td&gt;Webpack, Rollup, Nx&lt;/td&gt;
      &lt;td&gt;Vite, Gulp, Grunt, esbuild&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Go&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;go build, Make, Task, Bazel&lt;/td&gt;
      &lt;td&gt;Mage&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;PHP&lt;/td&gt;
      &lt;td&gt;Composer, Phing&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Robo, Deployer&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ruby&lt;/td&gt;
      &lt;td&gt;Rake, Bundler&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Thor, Hoe&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Swift&lt;/td&gt;
      &lt;td&gt;SwiftPM, Xcode&lt;/td&gt;
      &lt;td&gt;Bazel&lt;/td&gt;
      &lt;td&gt;Tuist&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Kotlin&lt;/td&gt;
      &lt;td&gt;Gradle, Maven&lt;/td&gt;
      &lt;td&gt;Bazel&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;TypeScript&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;tsc, Webpack, Rollup&lt;/td&gt;
      &lt;td&gt;esbuild, Vite&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;R&lt;/td&gt;
      &lt;td&gt;R CMD build, devtools&lt;/td&gt;
      &lt;td&gt;Make&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Rust&lt;/td&gt;
      &lt;td&gt;Cargo&lt;/td&gt;
      &lt;td&gt;Buck, Bazel, Make&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Scala&lt;/td&gt;
      &lt;td&gt;sbt, Gradle, Maven&lt;/td&gt;
      &lt;td&gt;Pants, Bazel&lt;/td&gt;
      &lt;td&gt;Mill&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Elixir&lt;/td&gt;
      &lt;td&gt;Mix&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;Rebar (from Erlang), Bake&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Haskell&lt;/td&gt;
      &lt;td&gt;Stack, Cabal&lt;/td&gt;
      &lt;td&gt;Bazel&lt;/td&gt;
      &lt;td&gt;Shake&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Dart&lt;/td&gt;
      &lt;td&gt;pub&lt;/td&gt;
      &lt;td&gt;Bazel (via rules_dart)&lt;/td&gt;
      &lt;td&gt;build_runner&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Erlang&lt;/td&gt;
      &lt;td&gt;Rebar3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;erlang.mk&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Zig&lt;/td&gt;
      &lt;td&gt;zig build system&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;CMake (rare, via wrappers)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;two-anemic-applications&quot;&gt;Two anemic applications&lt;/h1&gt;

&lt;p&gt;We’re going to build two contrived applications: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DirectedGraphBuildSystemsAreCool&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MonoreposRule&lt;/code&gt;. All they do is print to STDOUT then exit. Each only needs the letters in its own
class name, which is what gives us a nice non-trivial dependency graph without any actual business
logic to get in the way.&lt;/p&gt;

&lt;h1 id=&quot;components-that-the-applications-use&quot;&gt;Components that the applications use&lt;/h1&gt;

&lt;p&gt;The components are categorized into modules, each representing a type of phonetic sound:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Vowels&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;A&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;I&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;O&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;U&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Nasal&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;M&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;N&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Voiceless&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;P&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sonorants&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fricatives&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;S&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Labiodental&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;V&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;W&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Glides&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;H&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;J&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sibilants&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Z&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Velar&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;G&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Voiced&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;B&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since the last time I wrote this up, the sim went polyglot. It’s now &lt;strong&gt;five languages&lt;/strong&gt;, and some of
the components crossed a language boundary:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;vowelbase&lt;/strong&gt; bridges Java and &lt;strong&gt;Rust&lt;/strong&gt; via JNI. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VowelBase&lt;/code&gt; Java class loads a native library
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvowelbase.so&lt;/code&gt;, implemented in Rust, whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;printString&lt;/code&gt; wraps a string in parentheses. A
gratuitous use of Java-invoking-Rust, but it demonstrates a cross-language edge in the graph.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;nasal&lt;/strong&gt; now bridges Java and &lt;strong&gt;Go&lt;/strong&gt;. The Go module compiles &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c-shared&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libgonasal.so&lt;/code&gt;,
and the Java &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nasal&lt;/code&gt; component loads it via JNI.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;sonorants&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;L&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;R&lt;/code&gt;) is now a &lt;strong&gt;Kotlin&lt;/strong&gt; module rather than Java.&lt;/li&gt;
  &lt;li&gt;There are also TypeScript, C#, Python and even an Aether-language component in the tree for other
demos, but the two apps above don’t depend on them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-dependency-graph&quot;&gt;The dependency graph&lt;/h2&gt;

&lt;p&gt;Here’s the class-dependency graph for the two apps. Each app (top) depends only on the letter classes
in its own name; the letter classes live in phonetic-category component modules (middle); and two of
those modules cross a language boundary via JNI - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowels&lt;/code&gt; through the Rust &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvowelbase.so&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nasal&lt;/code&gt;
through the Go &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libgonasal.so&lt;/code&gt; - while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sonorants&lt;/code&gt; is Kotlin. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sibilants&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;labiodental&lt;/code&gt; are present
in the tree but neither app needs them, so a DAG build never touches them.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/monorepo-sim-class-dependencies.svg&quot; alt=&quot;Class dependency graph for the monorepo-sim apps&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;the-dag-side-aether-build&quot;&gt;The DAG side: Aether Build&lt;/h1&gt;

&lt;p&gt;I’m not going to use Bazel (&lt;a href=&quot;https://bazel.build/&quot;&gt;bazel.build&lt;/a&gt;, Google’s open-sourcing of Blaze) or
Nx (&lt;a href=&quot;https://nx.dev/&quot;&gt;nx.dev&lt;/a&gt;). I’m using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt;, which illustrates the two aspects of Blaze I most
remember from my nearly two years in Google’s Test Mercenaries team:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;a &lt;strong&gt;leaf-first directed (acyclic) graph&lt;/strong&gt; approach to building in a monorepo, and&lt;/li&gt;
  &lt;li&gt;a &lt;strong&gt;changed-or-not check&lt;/strong&gt; of inputs to skip individual steps.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;build-files&quot;&gt;Build files&lt;/h2&gt;

&lt;p&gt;Each module directory carries up to three declarative build files:&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;File&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Compile - declares deps, invokes the language compiler&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.ae&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Test - declares deps + test libs, compiles and runs tests&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dist.ae&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Package - builds a fat jar or other distributable&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Here’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java/applications/monorepos_rule/.build.ae&lt;/code&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-aether&quot;&gt;import build
import build (prereq)
import java

aeb(cap) {
    b = build.start()
    prereq(b, &quot;jdk:21&quot;)
    build.dep(b, &quot;java/components/fricatives/.build.ae&quot;)
    build.dep(b, &quot;java/components/nasal/.build.ae&quot;)
    build.dep(b, &quot;kotlin/components/sonorants/.build.ae&quot;)
    build.dep(b, &quot;java/components/voiceless/.build.ae&quot;)
    build.dep(b, &quot;java/components/vowels/.build.ae&quot;)
    java.javac(b)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Three things worth pointing at:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb(cap)&lt;/code&gt; is the entrypoint&lt;/strong&gt; - not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main()&lt;/code&gt;. The build receives a &lt;strong&gt;capability handle&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cap&lt;/code&gt;
from the trusted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; host (the same handle that backs runtime sandbox containment). A build file
never constructs its own authority; it only receives it. (The legacy &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main()&lt;/code&gt; spelling still lowers
to the same context-receiving entrypoint, but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb(cap)&lt;/code&gt; is the convention the repo demonstrates.)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prereq(b, &quot;jdk:21&quot;)&lt;/code&gt;&lt;/strong&gt; declares the toolchain this node needs, OS-agnostically. A remote agent’s
requester can read &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb --prereqs&lt;/code&gt; and pick, say, an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb-tc:rust&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb-tc:go&lt;/code&gt; image per
dispatch - the bare host need not have every toolchain installed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dependencies are one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build.dep(b, &quot;path&quot;)&lt;/code&gt; per line&lt;/strong&gt; - greppable, so the DAG can be extracted
without compiling anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cross-language edges are just more &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build.dep&lt;/code&gt; lines. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java/components/vowelbase/.build.ae&lt;/code&gt;
depends on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rust/components/vowelbase/.build.ae&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java/components/nasal/.build.ae&lt;/code&gt; depends on
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go/components/nasal/.build.ae&lt;/code&gt;. The Rust one is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo_project&lt;/code&gt; producing a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdylib&lt;/code&gt;; the Go one is
a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go_build&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c-shared&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;A single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; invocation scans the relevant &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.ae&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dist.ae&lt;/code&gt; files,
topologically sorts the dependency graph, and runs everything in &lt;strong&gt;one process&lt;/strong&gt; with an in-memory
visited-module map. No per-step shell fork storm, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.buildStepsDoneLastExecution&lt;/code&gt; file.&lt;/p&gt;

&lt;h2 id=&quot;first-build&quot;&gt;First build&lt;/h2&gt;

&lt;p&gt;Picking the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MonoreposRule&lt;/code&gt; app and asking for its distribution jar. First, a genuinely cold build —
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; keeps a &lt;strong&gt;content-addressed cache&lt;/strong&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aeb/cache&lt;/code&gt;, so to see everything actually compile I
clear both that and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rm -rf target ~/.aeb/cache
aeb java/applications/monorepos_rule/.dist.ae
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The telemetry summary:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[telemetry]
  dist:    java/applications/monorepos_rule 0.00s [n/a]
  build:   java/applications/monorepos_rule 0.00s [miss]
  build:   java/components/fricatives       0.00s [miss]
  build:   java/components/nasal            0.00s [miss]
  build:   kotlin/components/sonorants      0.00s [miss]
  build:   java/components/voiceless        0.00s [miss]
  build:   java/components/vowels           0.00s [miss]
  build:   go/components/nasal              0.00s [miss]
  build:   java/components/vowelbase        0.00s [miss]
  build:   rust/components/vowelbase        0.00s [miss]
  jni.crate: libs/rust/registry/vendor/jni    0.00s [n/a]
total: 15.80s wall
aeb: 9 compile + 1 dist + 0 test
  compile: java/components/fricatives
  compile: go/components/nasal
  compile: java/components/nasal
  compile: kotlin/components/sonorants
  compile: java/components/voiceless
  compile: rust/components/vowelbase
  compile: java/components/vowelbase
  compile: java/components/vowels
  compile: java/applications/monorepos_rule
  dist:    java/applications/monorepos_rule
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note what is &lt;strong&gt;not&lt;/strong&gt; there. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MonoreposRule&lt;/code&gt; app only needs the letters &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;M O N O R E P O S R U L E&lt;/code&gt;,
so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; built exactly the nine modules feeding those letters - Java &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fricatives&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nasal&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;voiceless&lt;/code&gt;/
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowels&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowelbase&lt;/code&gt;, Kotlin &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sonorants&lt;/code&gt;, Go &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nasal&lt;/code&gt;, Rust &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowelbase&lt;/code&gt; (plus the vendored &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jni&lt;/code&gt; crate)&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;and the app itself. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sibilants&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;labiodental&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consonants&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glides&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;velar&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;voiced&lt;/code&gt; are all
present in the filesystem but irrelevant to this target, so they never compiled. That skipping of
present-but-not-pertinent source is the defining trait of a DAG build system. Five languages, one
command, one linked binary orchestrating the lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;running-it-again&quot;&gt;Running it again&lt;/h2&gt;

&lt;p&gt;Run the same command a second time and every node is a cache &lt;strong&gt;hit&lt;/strong&gt;:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[telemetry]
  dist:    java/applications/monorepos_rule 0.00s [n/a]
  build:   java/applications/monorepos_rule 0.00s [hit]
  build:   java/components/fricatives       0.00s [hit]
  build:   java/components/nasal            0.00s [hit]
  build:   kotlin/components/sonorants      0.00s [hit]
  build:   java/components/voiceless        0.00s [hit]
  build:   java/components/vowels           0.00s [hit]
  build:   go/components/nasal              0.00s [hit]
  build:   java/components/vowelbase        0.00s [hit]
  build:   rust/components/vowelbase        0.00s [hit]
  jni.crate: libs/rust/registry/vendor/jni    0.00s [n/a]
total: 3.20s wall
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;15.8s cold, ~3.2s warm. The cache is keyed on the &lt;strong&gt;digest of the inputs&lt;/strong&gt;, not file timestamps - a
meaningful upgrade over the old bash sim, which used modification times. Because the cache lives in
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aeb/cache&lt;/code&gt; and not under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt;, blowing away &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt; alone still yields all-hits; you have to
clear the cache to force real recompilation. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[hit]&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[miss]&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[n/a]&lt;/code&gt; in the telemetry is the whole
incrementality story, right there.&lt;/p&gt;

&lt;h2 id=&quot;running-the-tests&quot;&gt;Running the tests&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aeb javatests/applications/monorepos_rule/.tests.ae
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[telemetry]
  tests:   javatests/applications/monorepos_rule 0.00s [miss] 1/1 PASS
  build:   java/applications/monorepos_rule 0.00s [hit]
  junit.jar: libs/java/junit                  0.00s [n/a]
  hamcrest.jar: libs/java/hamcrest               0.00s [n/a]
  ...
total: 4.16s wall
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.ae&lt;/code&gt; file depends on the app’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt; plus the vendored JUnit and Hamcrest jars, then
runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java.javac_test&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;java.junit&lt;/code&gt;. Because the app’s compile is already cached, only the test
compile-and-run actually happens.&lt;/p&gt;

&lt;h2 id=&quot;running-the-app&quot;&gt;Running the app&lt;/h2&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dist.ae&lt;/code&gt; step told us how to launch it. The fat jar carries all 1000-odd classes plus both
native libraries (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvowelbase.so&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libgonasal.so&lt;/code&gt;) at the jar root, so:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;java -Djava.library.path=. -jar monorepos-rule.jar
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;main() .. MonoreposRule instance created:
&amp;lt;M&amp;gt;(O)&amp;lt;N&amp;gt;(O){R}(E)P(O)S{R}(U){L}(E)
Key: (vowels via Rust), &amp;lt;nasal via Go&amp;gt;, {sonorants via Kotlin}, all others pure Java
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(...)&lt;/code&gt; are vowels routed through the Rust JNI library, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;...&amp;gt;&lt;/code&gt; are the nasal letters routed through
the Go shared library, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{...}&lt;/code&gt; are sonorants from the Kotlin module, and the bare letters are plain
Java. That single line of stdout is exercising four languages linked into one process. Then it exits.
That is all these apps do.&lt;/p&gt;

&lt;h2 id=&quot;building-the-other-application&quot;&gt;Building the other application&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aeb java/applications/directed_graph_build_systems_are_cool/.dist.ae
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DirectedGraphBuildSystemsAreCool&lt;/code&gt; needs a bigger alphabet (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consonants&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glides&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;velar&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;voiced&lt;/code&gt;
come into play), so its graph pulls in more modules - 13 compile targets versus 9. But everything it
shares with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MonoreposRule&lt;/code&gt; - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fricatives&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nasal&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sonorants&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;voiceless&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowels&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vowelbase&lt;/code&gt;
and its Rust/Go natives - is already a cache hit from the previous build. Only the genuinely new
modules compile. The fat jar it produces contains that app’s deps and none of the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MonoreposRule&lt;/code&gt;-only ones. It runs the same way:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;main() .. DirectedGraphBuildSystemsAreCool instance created:
D(I){R}(E)CT(E)DG{R}(A)PHB(U)(I){L}DSYST(E)&amp;lt;M&amp;gt;S(A){R}(E)C(O)(O){L}
Key: (vowels via Rust), &amp;lt;nasal via Go&amp;gt;, {sonorants via Kotlin}, all others pure Java
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h1 id=&quot;the-depth-first-recursive-side-maven&quot;&gt;The depth-first recursive side: Maven&lt;/h1&gt;

&lt;p&gt;The sim’s &lt;a href=&quot;https://github.com/paul-hammant/google-monorepo-sim/tree/depth-first_recursive_modular_monorepo&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;depth-first_recursive_modular_monorepo&lt;/code&gt; branch&lt;/a&gt;
has the same Java and Rust sources, in a classic modular layout, built by Maven.&lt;/p&gt;

&lt;p&gt;In a typical Maven build, modules are built from the root, resolving and building dependencies in the
right sequence. Maven encodes its build instructions in XML, one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pom.xml&lt;/code&gt; per module. It traverses
each module to its full depth of submodules before moving to the next sibling, in order to understand
the entire implicit graph. Intelligence from that graph lets Maven reorder modules so the
most-depended-on ones compile and test first. That’s a &lt;strong&gt;depth-first&lt;/strong&gt; visitation.&lt;/p&gt;

&lt;p&gt;Note: This branch is significantly out of date now. I had hope to make it a solid peer of the aeb-using one but that has not happened.&lt;/p&gt;

&lt;h2 id=&quot;a-full-clean-build&quot;&gt;A full clean build&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./quieter-mvn clean package
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;(that shell script is just &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mvn clean package&lt;/code&gt; with fewer lines of output.) Maven visits every module,
running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clean&lt;/code&gt;, compile, test-compile, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jar&lt;/code&gt; for each - dozens of steps - because
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package&lt;/code&gt; from the root touches the whole reactor. On my middle-of-the-pack AMD desktop that was about
11 seconds.&lt;/p&gt;

&lt;h2 id=&quot;an-incremental-build-no-clean&quot;&gt;An incremental build (no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clean&lt;/code&gt;)&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./quieter-mvn package
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Maven skips recompilation where sources haven’t changed (it compares source timestamps against
compiled classes - Rust included). But it still &lt;strong&gt;enters&lt;/strong&gt; the compile and Surefire plugins for every
module and still &lt;strong&gt;executes&lt;/strong&gt; tests regardless of whether that module’s sources changed, because a
linked dependency may have. The skip of the actual do-compile / run-tests happens &lt;em&gt;inside&lt;/em&gt; the plugin,
so the modules still get listed. About 5.5 seconds - roughly half the clean run.&lt;/p&gt;

&lt;h2 id=&quot;focusing-with--pl-and--am&quot;&gt;Focusing with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-pl&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-am&lt;/code&gt;&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./quieter-mvn package -pl applications/monorepos_rule -am
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-pl&lt;/code&gt; (project list) names the module to build; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-am&lt;/code&gt; (also-make) ensures its dependencies build too.
Maven still visits every module however deep to build understanding, then subsets the reactor to the
target and its deps - so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sibilants&lt;/code&gt; and friends are skipped. This is the closest Maven gets to the
DAG tools’ behaviour, and its build time here matches the equivalent focused &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; invocation.&lt;/p&gt;

&lt;p&gt;Maven’s &lt;strong&gt;Reactor&lt;/strong&gt; is the part that schedules module builds by dependency order and runs the
lifecycle phases - compile, test, package - within each. The key contrast with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt;: Maven’s reactor
reads the &lt;em&gt;whole&lt;/em&gt; source base up front to plan, whereas the DAG side has no scheduler surveying
everything first - it walks out from the requested target through its declared deps.&lt;/p&gt;

&lt;h1 id=&quot;comparing-features&quot;&gt;Comparing features&lt;/h1&gt;

&lt;p&gt;In a larger company’s monorepo, where the intent is to &lt;strong&gt;share code at the source level and minimize
reliance on a binary repository&lt;/strong&gt; like Nexus or Artifactory, you’d reach for a DAG build system —
Buck, Bazel, or Nx - for:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Incremental builds&lt;/strong&gt; - track input changes, rebuild only what’s affected.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remote caching and execution&lt;/strong&gt; - share artifacts across machines / teammates.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Hermetic builds&lt;/strong&gt; - same inputs, same outputs, regardless of environment.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Parallel execution&lt;/strong&gt; - independent modules across cores.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fine-grained dependency management&lt;/strong&gt; - deps declared at file, not just module, granularity.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cross-language support&lt;/strong&gt; - one graph spanning many languages.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reproducibility&lt;/strong&gt; - the DAG makes builds repeatable.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scalability&lt;/strong&gt; - handle huge numbers of modules and deps.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Custom build rules&lt;/strong&gt; - teams define their own build logic.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CI/CD integration&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Community and ecosystem&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt;-based sim, scored 0–5 against those:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;4&lt;/strong&gt; - real content-addressed caching now (digest of inputs, not timestamps), though it still
doesn’t do fine-grained &lt;em&gt;sub-file&lt;/em&gt; dependency analysis.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1&lt;/strong&gt; - a shared local cache exists (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.aeb/cache&lt;/code&gt;); no remote cache/execution.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2&lt;/strong&gt; - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prereq()&lt;/code&gt; declarations move it toward hermeticity (toolchain selection per node), but it’s
not sealed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3&lt;/strong&gt; - single-process topo-sorted execution; independent nodes can run without ceremony.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3&lt;/strong&gt; - module-level deps; Bazel can depend on individually-listed sources, which I don’t.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;5&lt;/strong&gt; - genuinely polyglot now: Java, Kotlin, Go, Rust and TypeScript in one graph, with JNI/FFI
edges across language boundaries. This is the big change since the bash version.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3&lt;/strong&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;3&lt;/strong&gt; - the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt; files are far less repetitive than the old bash scripts thanks to the SDK’s
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javac()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kotlinc()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go_build()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo_build()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsc()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shade()&lt;/code&gt; helpers.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2&lt;/strong&gt; - doable via the SDK; I haven’t shown a bespoke rule.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;4&lt;/strong&gt; - nothing about this would trouble CI; the reporting could be prettier.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;1&lt;/strong&gt; - it’s a teaching toy, not an ecosystem.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;circular-references&quot;&gt;Circular references&lt;/h2&gt;

&lt;p&gt;Neither class of build system allows circular references at the module level - both fail early on a
compile/test/package attempt. Some &lt;em&gt;languages&lt;/em&gt; allow circular references between files
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Laurel.java&lt;/code&gt; ↔ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Hardy.java&lt;/code&gt;), but that lives inside a single compiler invocation (one module), which
is a different thing.&lt;/p&gt;

&lt;h2 id=&quot;subsetting-the-checkout-expandcontract&quot;&gt;Subsetting the checkout (expand/contract)&lt;/h2&gt;

&lt;p&gt;Google’s secret sauce.&lt;/p&gt;

&lt;p&gt;The sim also demonstrates Git sparse-checkout, driven by Aether. A &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gcheckout&lt;/code&gt; tool recursively walks
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.ae&lt;/code&gt;, extracts every transitive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dep()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm_dep()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo_dep()&lt;/code&gt;,
and adds just those directories to the sparse checkout - so your working tree can hold one app and its
deps and nothing else, exactly as Google’s in-house Piper-based monorepo lets you expand and contract.
Earlier writing on this:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://paulhammant.com/2014/01/06/googlers-subset-their-trunk/&quot;&gt;Googlers Subset their Trunk&lt;/a&gt; (2014)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://paulhammant.com/2015/05/20/turning-bazel-back-into-blaze-for-monorepo-nirvana/&quot;&gt;Turning Bazel back into Blaze for monorepo nirvana&lt;/a&gt; (2015)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://paulhammant.com/2017/02/08/further-experiments-with-expanding-contracting-monorepos/&quot;&gt;Further Experiments With Expanding/Contracting Monorepos&lt;/a&gt; (2017)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;interpreted-config-vs-a-compiled-build-program&quot;&gt;Interpreted config vs a compiled build program&lt;/h2&gt;

&lt;p&gt;It’s worth being precise about &lt;em&gt;how&lt;/em&gt; the two DAG worlds turn build files into work, because Aeb and
Bazel sit on opposite sides of a line here.&lt;/p&gt;

&lt;p&gt;Bazel’s build files are written in &lt;strong&gt;Starlark&lt;/strong&gt; - a deliberately-constrained Python dialect (no
unbounded loops, no recursion, deterministic by construction). Each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BUILD&lt;/code&gt; file is &lt;strong&gt;parsed to an
AST&lt;/strong&gt;, and that AST is then &lt;strong&gt;interpreted&lt;/strong&gt; - tree-walked - by Bazel’s embedded Starlark interpreter.
The &lt;em&gt;output&lt;/em&gt; of that evaluation is data: the action graph Bazel’s engine subsequently schedules and
executes. There is no native-code compilation of your build logic, and no static type checking of it;
Starlark is dynamically typed and evaluated afresh on each invocation.&lt;/p&gt;

&lt;p&gt;Aeb is a different pipeline. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.build.ae&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.tests.ae&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.dist.ae&lt;/code&gt; files are &lt;strong&gt;Aether source&lt;/strong&gt;,
and a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb&lt;/code&gt; invocation hands them (plus the build SDK - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;javac()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kotlinc()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go_build()&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cargo_build()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsc()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shade()&lt;/code&gt; and friends) to the Aether compiler (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ae&lt;/code&gt;), which
&lt;strong&gt;compiles and links the whole thing into one native binary&lt;/strong&gt; and then &lt;strong&gt;executes it&lt;/strong&gt;. The build
logic itself becomes native code, not a graph fed to an interpreter. Because it goes through a real
compiler, the build files are &lt;strong&gt;statically type-checked&lt;/strong&gt; on the way through - that’s the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Type checking completed with N warning(s)&lt;/code&gt; chatter you see scroll past during a build, catching (for
instance) an unused variable in a build file before anything runs. The whole dependency graph is
topologically sorted and driven from that single process with its in-memory visited-module map.&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Starlark (Bazel)&lt;/th&gt;
      &lt;th&gt;Aeb (Aether)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Pipeline&lt;/td&gt;
      &lt;td&gt;source → AST → &lt;strong&gt;interpret&lt;/strong&gt; (tree-walk)&lt;/td&gt;
      &lt;td&gt;source → &lt;strong&gt;compile + link to native binary&lt;/strong&gt; → execute&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Build logic is&lt;/td&gt;
      &lt;td&gt;data producing an action graph&lt;/td&gt;
      &lt;td&gt;a compiled native program&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Type checking&lt;/td&gt;
      &lt;td&gt;none (dynamically typed)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;static&lt;/strong&gt;, at compile time&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Conditionals&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;elif&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;else&lt;/code&gt; - but only inside functions, and no way to run at “load” time beyond simple selects&lt;/td&gt;
      &lt;td&gt;full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt; (and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;-expressions), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;match&lt;/code&gt; dispatch, guarded function clauses, &lt;em&gt;and&lt;/em&gt; a compile-time &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt; static-if that only type-checks the taken arm&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Loops&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;for&lt;/code&gt; over a sequence, plus comprehensions - &lt;strong&gt;no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt;, no recursion&lt;/strong&gt; (bounded by design)&lt;/td&gt;
      &lt;td&gt;unrestricted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;while&lt;/code&gt; (Turing-complete), head/tail list destructuring, higher-order closures - it’s a real systems language&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Execution&lt;/td&gt;
      &lt;td&gt;interpreter re-evaluates &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BUILD&lt;/code&gt; files&lt;/td&gt;
      &lt;td&gt;one linked binary, single process&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Neither is “the right answer” - and it’s the sort of distinction that gets hand-waved as “they’re both
just DAG build tools.” They parse and evaluate your intent very differently. The place people assume
Bazel wins outright is &lt;em&gt;containment&lt;/em&gt;: Starlark’s interpretation and Bazel’s sandboxed action execution
buy hermeticity guarantees, and you’d expect a “little compiled binary” to make no attempt at that. But
that assumption is where Aeb is actually most interesting.&lt;/p&gt;

&lt;h2 id=&quot;where-this-is-heading-containment&quot;&gt;Where this is heading: containment&lt;/h2&gt;

&lt;p&gt;Look again at that entrypoint - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb(cap)&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main()&lt;/code&gt;. The build receives a &lt;strong&gt;capability handle&lt;/strong&gt;
from the trusted host and never constructs its own authority. That isn’t cosmetic. Aether ships a
whole &lt;a href=&quot;https://github.com/aether-lang-org/aether/blob/main/docs/containment-sandbox.md&quot;&gt;containment model&lt;/a&gt;:
a deny-by-default grant DSL (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grant_fs_read&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grant_tcp&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grant_exec&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;grant_env&lt;/code&gt;), enforced at
three layers - the compiler refusing capability imports under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--emit=lib&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hide&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;seal except&lt;/code&gt; at
lexical-scope level, and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LD_PRELOAD&lt;/code&gt; shim (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libaether_sandbox.so&lt;/code&gt;) that intercepts libc against
the grant list, with a seccomp-bpf fence closing the raw-&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clone3&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfork&lt;/code&gt; process-creation bypass. The
containment principle is Avalon-style Inversion of Control: the container wires the grants, the
contained code merely receives them and can’t tell it’s boxed.&lt;/p&gt;

&lt;p&gt;Point that at a &lt;em&gt;build system&lt;/em&gt; and the picture almost writes itself. A build action is exactly a piece
of code that should only touch its declared inputs, write only its declared outputs, and - for most
targets - reach no network at all. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prereq(b, &quot;rust:1.75&quot;)&lt;/code&gt; line already declares the toolchain a
node needs OS-agnostically; the obvious next move is that each node runs under a sandbox scope derived
from its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dep()&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prereq&lt;/code&gt; declarations: read access to exactly its sources and dependency artifacts,
write access to exactly its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target/&lt;/code&gt; slot, exec of exactly its compiler, and nothing else - a build
step that tries to phone home or read &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/shadow&lt;/code&gt; mid-compile is denied by construction, not by
policy review. That’s Bazel’s hermeticity, but arrived at from the &lt;em&gt;language’s&lt;/em&gt; capability model rather
than from a syscall-emulating action runner.&lt;/p&gt;

&lt;p&gt;And the repo already leans this way. There’s a
&lt;a href=&quot;https://github.com/aether-lang-org/aeb/blob/main/AEB-MEGADAG-VISION.md&quot;&gt;vision doc&lt;/a&gt; describing the
large version - ingesting 141 strangers’ Todo-Backend implementations across 30+ languages into one DAG
with &lt;strong&gt;hermetic per-implementation containers, cloud-agent fan-out, and supply-chain veto on untrusted
code&lt;/strong&gt; - and the scaffolding for it is already in the source: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/sandbox/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/agent/&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/provision/&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veto/&lt;/code&gt; tree. The toolchain-selection story (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb --prereqs&lt;/code&gt; picking a
per-dispatch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb-tc:rust&lt;/code&gt; image) is the same
capability handle flowing outward to a remote build agent. So my prediction is straightforward: the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb(cap)&lt;/code&gt; handle grows from “the thing that runs your build files” into “the thing that &lt;em&gt;contains&lt;/em&gt;
them” - per-node grants, containerised remote execution, and a veto layer over untrusted third-party
build files - and the compiled-binary model turns out to be an &lt;em&gt;advantage&lt;/em&gt; here, because the sandbox is
the same one the language already enforces at compile time, not a separate runtime wrapper bolted on.&lt;/p&gt;

&lt;p&gt;The honest caveat: today none of that is wired into the build path I ran above - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aeb(cap)&lt;/code&gt; receives the
handle but the per-node grant plumbing is vision, not shipped. Bazel’s sandboxing is real and running
now; Aeb’s is a credible trajectory with the primitives already in the tree. But “they’re both just DAG
build tools” misses that one of them is a config language that produces an action graph, and the other
is a capability-secure systems language that could end up &lt;em&gt;being&lt;/em&gt; the sandbox.&lt;/p&gt;

&lt;h2 id=&quot;a-note-on-scale&quot;&gt;A note on scale&lt;/h2&gt;

&lt;p&gt;The modules here are not representative of enterprise sizes - each is a handful of tiny classes with a
single placeholder test. In reality there could be hundreds of sources per module and tens of seconds
of compile and test each. For Google’s thousands of applications and libraries with high test
coverage, a from-root build of everything could take days on one workstation - and is never done, even
with Blaze, given the trunk checkout was 90 GB back in 2012 and no dev workstation ever pulled all of
it. That’s exactly why expand/contract and a leaf-first DAG matter.&lt;/p&gt;

&lt;p&gt;This repo goes hand in hand with my book,
&lt;a href=&quot;https://tbd-book.com/&quot;&gt;Trunk-Based Development and Branch by Abstraction&lt;/a&gt;, and
&lt;a href=&quot;https://tbd-book.com/gmr-vid&quot;&gt;a short video&lt;/a&gt; about it … though that last is the older shell-script version thats now deleted even if the experience was the same.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Cherry-pick Asymmetry</title>
   <link href="https://paulhammant.com/2026/06/22/the-cherry-pick-asymmetry/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/06/22/the-cherry-pick-asymmetry</id>
   <content type="html">&lt;p&gt;Context is Trunnk-Based Development with Branch for Relase, and cherry picking. Trunk is main or master for may Git repos, of course.&lt;/p&gt;

&lt;p&gt;A friend put a scenario to me and I gave him a strong answer: I said it was &lt;strong&gt;statistically impossible&lt;/strong&gt; for the bug to still be on trunk once formal QA had signed it off 
on the release-branch-coupled CD environment. He raised an eyebrow. This post is me making good on that claim - and being honest about the tiny sliver where 
“impossible” is really “one-in-a-few-thousand,” and what that sliver actually is.&lt;/p&gt;

&lt;p&gt;The scenario. You’re doing trunk-based development with a &lt;a href=&quot;//paulhammant.com/2015/12/13/trunk-based-development-when-to-branch-for-release/&quot;&gt;branch for release&lt;/a&gt;. 
Cherry-picks go &lt;strong&gt;one way only&lt;/strong&gt; - trunk to release branch, never back. A bug is found. The fix goes onto trunk, gets a 
&lt;strong&gt;desk-check&lt;/strong&gt; sign-off (someone stands the site up on localhost, clicks around, says “yep, fixed”), and then it’s cherry-picked to the release branch. 
From there Jenkins (or similar) continuous-deploys it into a coupled “release-candidate” environment, and a QA colleague does a &lt;strong&gt;formal&lt;/strong&gt; sign-off against the bug
in &lt;em&gt;that&lt;/em&gt; environment. All that in addition to test automation that the same Jenkins performed for the commit(s).&lt;/p&gt;

&lt;p&gt;So the same fix now lives on two branches, but with two very different levels of scrutiny:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Trunk:&lt;/strong&gt; desk-check only. One developer, localhost, eyeballs, and automated tests appreciated.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Release branch:&lt;/strong&gt; desk-check (inherited via the cherry-pick) &lt;strong&gt;plus&lt;/strong&gt; a formal QA pass in a real deployed environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My friend’s question: &lt;strong&gt;what’s the statistical chance the bug is still present in trunk&lt;/strong&gt;, given it’s been formally signed off as gone on the release branch? 
My answer: effectively zero, and &lt;em&gt;not&lt;/em&gt; by luck - by construction.&lt;/p&gt;

&lt;h2 id=&quot;why-its-almost-impossible-not-merely-unlikely&quot;&gt;Why it’s (almost) impossible, not merely unlikely&lt;/h2&gt;

&lt;p&gt;This is not a “did the fix get applied?” question. The fix is the &lt;em&gt;same commit&lt;/em&gt; on both branches. A clean cherry-pick puts the identical diff on the release branch - 
and crucially, &lt;strong&gt;if the cherry-pick landed without a clash, trunk’s copy and release’s copy of the fix are the same bytes against compatible context.&lt;/strong&gt; If those identical 
bytes pass formal QA on release, the very same bytes are sitting in trunk. There is no mechanism by which “this exact change works there but not here” can be true 
when &lt;em&gt;there&lt;/em&gt; and &lt;em&gt;here&lt;/em&gt; hold the same change. That’s the “impossible” - and it’s a structural fact about cherry-picking from trunk, not a probability. At least, it is when using
a modern VCS with merge point tracking.&lt;/p&gt;

&lt;p&gt;So the residual risk is &lt;strong&gt;not&lt;/strong&gt; “the fix doesn’t work on trunk.” It’s two things, both of which are &lt;em&gt;not the bug you fixed&lt;/em&gt;:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;The formal check didn’t catch it.&lt;/strong&gt; The sign-off said “fixed” but it wasn’t - a flaky repro, the environment happened to mask it, the wrong path got exercised. Then it’s not fixed &lt;em&gt;anywhere&lt;/em&gt;, release included, and the premise “formal QA signed it off” carried less information than we assumed. Worth being careful here about &lt;em&gt;why&lt;/em&gt;: this is rarely anyone slacking. The honest cause is usually &lt;strong&gt;scale&lt;/strong&gt;. A manual gate that was statistically airtight for a small app and a small team gets asked to span more surface per release as the portfolio of change grows with the company - the same gate, now covering more. Holding the old bar at the new volume would have meant doubling or tripling QA headcount, which orgs almost never do; so the gap widens as an &lt;em&gt;under-investment decision made above the team&lt;/em&gt;, not a failing within it. It’s a property of how the verification model was funded against growth, not of the branching model. (The CRT’s &lt;a href=&quot;//paulhammant.com/2021/02/19/software-development-current-reality-tree-starter-pack/&quot;&gt;&lt;em&gt;“started with manual QA for a smaller application and dev team”&lt;/em&gt;&lt;/a&gt; node is exactly this.)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Trunk has since become a different codebase&lt;/strong&gt; - a &lt;em&gt;later&lt;/em&gt; commit re-broke it, or the surrounding code the fix relies on drifted. That’s a &lt;strong&gt;new&lt;/strong&gt; defect on trunk, not the one you fixed surviving.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither contradicts the claim. The specific bug, once its cherry-pick clears QA on release, &lt;strong&gt;cannot&lt;/strong&gt; be the thing still broken on trunk. Put a number on the residual anyway, to give you a feel: for a well-resourced QA gate and a tight cherry-pick loop, you’re in &lt;strong&gt;1-in-10,000 to 1-in-100,000&lt;/strong&gt; territory; for a gate stretched thin by a grown portfolio of change, or a long-lived branch where trunk has drifted hard, it climbs toward &lt;strong&gt;1-in-1,000&lt;/strong&gt;. Those are the odds of one of the two &lt;em&gt;other&lt;/em&gt; failures above - never of the fixed bug itself reappearing on trunk.&lt;/p&gt;

&lt;h2 id=&quot;the-divergence-mechanisms---ie-the-only-ways-to-lose-the-bet&quot;&gt;The divergence mechanisms - i.e. the only ways to lose the bet&lt;/h2&gt;

&lt;p&gt;These are the only routes by which the formal release sign-off fails to cover trunk. Read them as &lt;em&gt;ways trunk became a different codebase or QA was wrong&lt;/em&gt; - not as ways the fix failed to transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Trunk moved after the desk-check, in the same region.&lt;/strong&gt;
The fix was desk-checked on trunk at commit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X&lt;/code&gt;. By the time you cherry-picked, trunk was at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X+n&lt;/code&gt;. If one of those &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; commits touched the same code the fix depends on - refactored the function, changed a caller, altered a shared helper - then trunk’s &lt;em&gt;current&lt;/em&gt; behaviour is no longer what was desk-checked. The release branch is frozen-ish and got the clean cherry-pick; trunk kept moving. &lt;strong&gt;The release branch can be more correct than trunk precisely because it stopped changing.&lt;/strong&gt; This is the big one, and it scales with your cherry-pick latency and your trunk commit rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The same hunks landed against different surrounding chunks.&lt;/strong&gt;
Covered in depth in &lt;a href=&quot;//paulhammant.com/2026/04/28/limits-of-merging-experiment/&quot;&gt;the limits-of-merging experiment&lt;/a&gt;. A clean cherry-pick isn’t really a merge - it’s a two-way apply: the fix’s &lt;em&gt;changed&lt;/em&gt; hunks go in, and the surrounding chunks are taken as-is, unchallenged. Nothing reconciles them. So the very same hunks can land against release’s surrounding code and against trunk’s surrounding code, and if those untouched chunks differ - because trunk drifted (mechanism #1) - the &lt;em&gt;effective&lt;/em&gt; behaviour differs even though the patch “applied cleanly” on both. The fix’s lines are identical; the code they execute against is not. Clean apply is necessary, not sufficient. Low probability, nasty when it hits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And here is the tell.&lt;/strong&gt; When the surrounding code has drifted &lt;em&gt;enough&lt;/em&gt;, the cherry-pick doesn’t apply cleanly - you get a &lt;strong&gt;merge clash&lt;/strong&gt;. That clash is not an annoyance to be force-resolved at 4pm on release day; it is &lt;em&gt;the system telling you the two branches have diverged in the fix’s blast radius&lt;/em&gt;. A clash is the loud, honest version of mechanism #2 - the same divergence that, when it’s just a hair smaller, applies “cleanly” and lies to you. So the clash is a gift: it converts a silent risk into a visible one. The mistake is treating a clash as friction rather than as intelligence. Which is the whole argument for a &lt;strong&gt;dry-run&lt;/strong&gt;, below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Trunk took a second commit that the release branch never got.&lt;/strong&gt;
Here’s the version of this that sounds paradoxical until you look at it on a timeline. Suppose trunk has &lt;strong&gt;two&lt;/strong&gt; commits in play: the fix (cherry-picked to release) and a &lt;em&gt;separate, later&lt;/em&gt; commit that re-breaks the same thing - a regression, or a new second instance of the same defect. The obvious objection: &lt;em&gt;if QA passed in the release-coupled CD env, how can the bug still be live in trunk?&lt;/em&gt; And the answer is clean - because the two commits are &lt;strong&gt;not both on the release branch&lt;/strong&gt;. Only the fix was cherry-picked. The release branch is fix-and-nothing-else: QA stands it up, drives the repro, signs off correctly. Trunk is fix-&lt;em&gt;plus&lt;/em&gt;-regressor.&lt;/p&gt;

&lt;p&gt;So how did the desk-check on trunk miss it? Because of &lt;em&gt;when&lt;/em&gt; it ran. The desk-check happened at the fix commit - and at that moment trunk really was fixed, and the desk-checker was telling the truth. The regressor landed afterwards. The desk-check didn’t fail; it &lt;strong&gt;expired&lt;/strong&gt;. Trunk is broken, release is green, both “passed” their respective checks, and there’s no contradiction - the checks ran at different points on a moving trunk, and only trunk kept moving.&lt;/p&gt;

&lt;p&gt;Notice this is not a half-fix arriving on the release branch. The fix arrived whole and works there. The trouble is entirely on trunk, and it is just mechanism #1 (drift) with the clock made explicit: &lt;strong&gt;a human sign-off is true at one commit; trunk is a moving target; the release branch stopped moving and the desk-check didn’t get a do-over.&lt;/strong&gt; That’s the whole answer to “can it be green on release and still broken in trunk” - yes, and only this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Environment-specific masking, in the other direction.&lt;/strong&gt;
The release-candidate environment is “coupled” and real; localhost is not. There are bugs that only manifest with the real environment’s data, config, or integrations. The formal QA pass in the RC environment can therefore &lt;em&gt;prove the fix more thoroughly than the desk-check ever could&lt;/em&gt; - which means the desk-check on trunk may have signed off on a fix that the desk-checker was a little optimistic on. Trunk inherits a weaker proof. The bug could still be “present” on trunk in the sense that &lt;strong&gt;nobody actually verified it there&lt;/strong&gt; under conditions that could expose it.&lt;/p&gt;

&lt;h2 id=&quot;so-whats-the-number&quot;&gt;So what’s the number?&lt;/h2&gt;

&lt;p&gt;Remember what the number is the probability &lt;em&gt;of&lt;/em&gt;: not “the fixed bug survived on trunk” (that’s the impossible part), but “the formal check didn’t catch it, &lt;strong&gt;or&lt;/strong&gt; trunk quietly became a different codebase in the fix’s blast radius.” With that framing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Tight loop, ‘on it’ QA team, thin slices&lt;/strong&gt; (cherry-pick minutes-to-hours after the desk-check, short-lived release branch): you’re at roughly &lt;strong&gt;1-in-100,000&lt;/strong&gt;. The fix is the same bytes against near-identical context; QA passed; trunk hasn’t moved meaningfully. About as close to “impossible” as software gets.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ordinary shop&lt;/strong&gt; (cherry-pick same-day, busy trunk, decent-but-human QA): call it &lt;strong&gt;1-in-10,000&lt;/strong&gt;. Drift is small but nonzero; quality assurance is good but not perfect.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Long-lived release branch, hard-drifted trunk, a verification gate stretched past what it was sized for&lt;/strong&gt;: it climbs toward &lt;strong&gt;1-in-1,000&lt;/strong&gt;, dominated by mechanism #1 (drift) - trunk became a different codebase while the branch sat still. Even here it’s &lt;em&gt;trunk drift or a missed check&lt;/em&gt; doing the work, never the fixed bug reappearing - and the “missed check” end is a scale-and-staffing story (the portfolio grew, the gate didn’t), not a worse team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There’s a fourth axis cutting across all three: &lt;strong&gt;how good is your test automation, and what shape is it?&lt;/strong&gt; A healthy &lt;a href=&quot;//paulhammant.com/2017/02/05/ui-component-testing/&quot;&gt;test pyramid&lt;/a&gt; - broad fast unit coverage, a solid integration band, a thin full-stack testing cap/peak - attacks &lt;em&gt;both&lt;/em&gt; residual terms at once. It shrinks &lt;strong&gt;P(QA wrong)&lt;/strong&gt; because the formal sign-off isn’t a lone manual gesture; the same behaviour is asserted by a unit test and an integration test that run on every commit, so a false negative has to fool all three. And it shrinks &lt;strong&gt;P(drift)&lt;/strong&gt; because the moment a later trunk commit re-breaks the thing (mechanism #3) or shifts the surrounding code (mechanism #2), trunk’s own CI goes red - you find out in minutes, not in next quarter’s release. An &lt;a href=&quot;https://mike-bland.com/2023/09/13/the-inverted-test-pyramid.html&quot;&gt;inverted pyramid&lt;/a&gt; does the opposite: it pushes you toward the 1-in-1,000 end &lt;em&gt;and&lt;/em&gt; makes the dry-run and the trunk regression test below far more valuable, because they’re compensating for coverage the &lt;a href=&quot;https://martinfowler.com/bliki/TestPyramid.html&quot;&gt;pyramid&lt;/a&gt; should have provided. So read the three rows above as “× your pyramid”: a fat-pyramid ordinary shop behaves like the tight-loop row; an full stack heavy tight-loop dev shop behaves like the ordinary row.&lt;/p&gt;

&lt;p&gt;The headline I’d give my friend, sharpened: &lt;strong&gt;a formal QA pass on the release branch proves the fix bytes work; because those exact bytes (and solidly indicated commit)  are also in trunk, the only ways trunk can still be broken are a false QA pass or a later change to trunk - and neither is the bug you fixed.&lt;/strong&gt; That’s why I called it statistically impossible and stand by it. The residual is a rounding error with three dials - &lt;em&gt;QA bar&lt;/em&gt;, &lt;em&gt;drift&lt;/em&gt;, and &lt;em&gt;test-automation shape&lt;/em&gt;  and a deep pyramid turns all three down at once.&lt;/p&gt;

&lt;h2 id=&quot;cadence-is-the-master-variable&quot;&gt;Cadence is the master variable&lt;/h2&gt;

&lt;p&gt;Trunk and the release branch are &lt;em&gt;strongly&lt;/em&gt; related - they share the entire history up to the branch point and only diverge afterwards. Every divergence mechanism above is really one quantity wearing different hats: &lt;strong&gt;how far has trunk drifted from the branch point by the time this cherry-pick happens?&lt;/strong&gt; And that distance is governed almost entirely by &lt;strong&gt;release cadence&lt;/strong&gt;. At least, &lt;strong&gt;planned&lt;/strong&gt; release cadende more so than unplanned.&lt;/p&gt;

&lt;p&gt;Compare two shops shipping the same software:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;One release branch a year, ~500 cherry-picks&lt;/strong&gt; (feeding, say, ~100 unplanned point releases off that one long-lived branch). By cherry-pick #400, trunk is &lt;em&gt;months&lt;/em&gt; ahead of the branch point. The surrounding chunks (mechanism #2) have been rewritten, callers have moved (mechanism #1), the bug may have sprouted a second home (mechanism #3) that didn’t exist when the branch was cut. Every cherry-pick is fighting a trunk that’s drifted enormously. The release branch and trunk are &lt;em&gt;technically&lt;/em&gt; still related, but the relationship is distant, and the release sign-off’s evidential value for trunk is correspondingly weak. This is the troublesome situation. The first time the merge engine says can’t merge without arbitration, all bets are off - the quantum link bwteeen the two testing places (two branches) is lost..&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Twelve release branches a year, a few cherry-picks each.&lt;/strong&gt; Each branch is only ever cut a few weeks before its cherry-picks land, so trunk has barely moved relative to the branch point. The same fix lands against near-identical surrounding code on both branches. Drift is small, so all four mechanisms shrink toward zero, and the formal sign-off on release transfers to trunk almost perfectly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same total release count, same total cherry-picks-ish - wildly different risk. The number of &lt;em&gt;branches&lt;/em&gt; per year is a proxy for &lt;em&gt;short distance between trunk and each branch&lt;/em&gt;, which is the thing that actually keeps the cherry-pick honest. This is the quantitative version of the advice in &lt;a href=&quot;//paulhammant.com/2026/04/28/limits-of-merging-experiment/&quot;&gt;the limits-of-merging post&lt;/a&gt;: long-lived release branches that absorb selective fixes are a structural risk, and the structure is &lt;em&gt;drift&lt;/em&gt;. Branch more often, keep each branch close to trunk, delete it fast.&lt;/p&gt;

&lt;p&gt;But “branch more often” is easy to say and hard to do, and the reasons it’s hard are exactly the constraints I mapped in the &lt;a href=&quot;//paulhammant.com/2021/02/19/software-development-current-reality-tree-starter-pack/&quot;&gt;software-development Current Reality Tree starter pack&lt;/a&gt;. A team can’t dial cadence up - can’t cut twelve short-lived branches a year instead of one long-lived one - until it has cleared the bottlenecks upstream of release frequency. Four nodes in that tree map straight onto this post’s dials:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Build times are too long&lt;/strong&gt; (“we build too much per build”, “the full build compiles too much code”, “integration tests are too slow”). This one I &lt;em&gt;didn’t&lt;/em&gt; call out above, but it’s load-bearing. Put a number on “too long”: say it’s &lt;strong&gt;two hours from dev-done to the change being stood up in a QA environment and tested&lt;/strong&gt; - compile, full test suite, deploy to the coupled env, then QA can start. When that round trip is two hours, you can’t afford to cut and verify a fresh branch per release, so you keep one alive and let trunk drift away from it; and every dry-run and every re-test of a cherry-pick costs another two hours, so people do fewer of them. Slow builds &lt;em&gt;force&lt;/em&gt; the long-lived-branch shape that maximises mechanism #1, and they tax the very mitigations (dry-run, re-test against trunk) that drain the residual. Get that round trip down to minutes and the whole calculus flips. This is the &lt;a href=&quot;//paulhammant.com/2017/06/22/devops-improvements-the-reduction-of-cycle-times/&quot;&gt;reduction-of-cycle-times&lt;/a&gt; argument wearing a different hat: those cycles nest like russian dolls, and your effective build time is the innermost one that sets a floor under all the others - cherry-pick latency and release cadence included. Shrink the build and every outer cycle &lt;em&gt;can&lt;/em&gt; shrink; leave it at two hours and none of them can.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Not enough QA is automated&lt;/strong&gt; (“over-reliance on manual testing”, “not enough QA automators”, “QA test suites not run that frequently”). This is the same fourth-axis point as the test pyramid above, seen from the constraint side: a thin or inverted pyramid is &lt;em&gt;why&lt;/em&gt; the formal QA pass is a lone manual gesture, which is &lt;em&gt;why&lt;/em&gt; P(QA wrong) doesn’t shrink.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The release process itself is error-prone / “we don’t practice releases safely”&lt;/strong&gt; - no rehearsals. Rehearsal is a whole family: dress-rehearsal deploys to a prod-like environment, blue-green and canary cutovers practised before they’re needed, rollback and roll-forward drills, smoke-test runbooks, game-days. The &lt;strong&gt;dry-run cherry-pick&lt;/strong&gt; below is just the cheapest, narrowest member of that family - it rehearses one question (does this fix apply cleanly against the branch?), not the deploy. But the muscle is the same: a team that never rehearses &lt;em&gt;anything&lt;/em&gt; is a team that discovers the clash at 4pm on release day.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CI isn’t actually continuous (per commit), and the branching model is crappy.&lt;/strong&gt; If trunk’s CI doesn’t run on every commit, mechanism #3 (a later regressor) goes undetected for longer; and a weak branching model is the soil all of this grows in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the residual probability in this post and the slow-cadence at the &lt;em&gt;top&lt;/em&gt; of that CRT are the same phenomenon viewed at two altitudes. The cherry-pick asymmetry is what you measure once a fix is in flight; the CRT is the map of why the fix is in flight on a branch that’s drifted too far in the first place.&lt;/p&gt;

&lt;h2 id=&quot;the-genuinely-counterintuitive-bit&quot;&gt;The genuinely counterintuitive bit&lt;/h2&gt;

&lt;p&gt;Why do we cherry-pick trunk → release exclusively in the first place? Not because we distrust the release branch - it’s about &lt;strong&gt;forgetting&lt;/strong&gt;. If you let people fix &lt;em&gt;on&lt;/em&gt; the release branch and rely on merging the fix back to trunk afterwards, you will eventually forget to do it. The fix ships, QA signs off, the client pops the champagne - and three months later the next release is cut from a trunk that never got the fix, and the bug walks straight back in. A regression, manufactured by a missed merge-back. Cherry-picking &lt;em&gt;from&lt;/em&gt; trunk forces the fix to exist in trunk &lt;strong&gt;by construction&lt;/strong&gt;: there’s no merge-back step to forget, so that class of regression is structurally impossible. The direction of the arrow &lt;em&gt;is&lt;/em&gt; the safety mechanism.&lt;/p&gt;

&lt;p&gt;But look at what that buys you on &lt;strong&gt;this one bug, at this one moment&lt;/strong&gt;: the causality inverts. The release branch is now the &lt;em&gt;better-verified&lt;/em&gt; artifact. It got the formal pass; trunk got an eyeball. The fix is &lt;em&gt;definitely&lt;/em&gt; in trunk - that’s the impossible-to-be-broken part - but the formal &lt;em&gt;evidence that it works&lt;/em&gt; lives on the release branch, not on trunk. Trunk has the cure and a weaker certificate; release has the same cure and the strong certificate. They share the fix; they don’t share the proof.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;not&lt;/strong&gt; an argument to start merging release back to trunk - that’s exactly the missed-merge-back regression trap the one-way rule exists to prevent (plus every &lt;a href=&quot;//paulhammant.com/2026/04/28/limits-of-merging-experiment/&quot;&gt;merge-tracking&lt;/a&gt; hazard on top). The fix is in trunk already. What’s missing from trunk is the &lt;em&gt;verification&lt;/em&gt;, and the cure for that is cheap:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Dry-run the cherry-pick early - well ahead of the moment of nerves.&lt;/strong&gt; First, the honest caveat: most cherry-picks just apply, cleanly, every time - if that’s your experience, you don’t need this as standing ceremony, and a same-day cherry-pick is already most of a dry-run. This earns its keep only for teams with a &lt;em&gt;track record&lt;/em&gt; of cherry-picks not being seamless - the long-lived-branch, hard-drifted-trunk shops at the 1-in-1,000 end, where a clash is a regular event rather than a rarity. &lt;em&gt;Those&lt;/em&gt; teams should: long before you need it, do a throwaway cherry-pick (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry-pick --no-commit&lt;/code&gt; and throw it away, or a scratch branch) just to ask one question: &lt;em&gt;does it clash?&lt;/em&gt; A clean apply is reassurance; a &lt;strong&gt;clash is intelligence&lt;/strong&gt; - it’s mechanism #2 announcing that trunk and the release branch have diverged in the fix’s blast radius, which is precisely when a “clean” apply would have lied to you. Rehearsing the cherry-pick ahead of time converts a 4pm-on-Friday panic into a calm Tuesday decision. The clash is information you &lt;em&gt;want&lt;/em&gt;, as early as you can get it.&lt;/li&gt;
  &lt;li&gt;If quick and cheap, &lt;strong&gt;run the same formal check against trunk&lt;/strong&gt;, or at least the automated portion of it. If the bug is worth a formal QA sign-off on release, it’s worth an automated regression test that runs in trunk’s CI forever. Then trunk’s coverage of this bug stops decaying - it’s pinned by a test, and the 1-in-10,000 drops further.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cherry-pick fast.&lt;/strong&gt; Every hour between desk-check and sign-off is more trunk drift (mechanism #1). Short loops shrink the dominant residual term. Perhaps only a problem for Google-sized companies with select teams doing branch-for-release.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Keep the slices thin.&lt;/strong&gt; A single-commit fix that can’t be partially-applied removes mechanism #3 entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;when-is-the-second-pass-actually-redundant&quot;&gt;When is the second pass actually redundant?&lt;/h2&gt;

&lt;p&gt;This is the part my friend and I went around on. Frame it as sets: the desk-check covers a region &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt; (localhost, one moment); the formal QA pass covers a region &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q&lt;/code&gt; (coupled environment, one moment). The bug is a point. If &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q ⊆ D&lt;/code&gt; - the release pass touches nothing the desk-check didn’t - then the second test adds zero bits and &lt;em&gt;is&lt;/em&gt; pure ceremony. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;D&lt;/code&gt; only &lt;strong&gt;partially overlap&lt;/strong&gt;: the coupled environment reaches behaviours localhost can’t (mechanism #4), so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q \ D&lt;/code&gt; is usually non-empty, and a manual pass is also a &lt;em&gt;snapshot&lt;/em&gt; that decays the instant trunk moves (the desk-check “expires”).&lt;/p&gt;

&lt;p&gt;What decides the size of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q \ D&lt;/code&gt; and whether it stays small isn’t the manual pass at all - it’s &lt;strong&gt;what automated coverage exists for this exact feature/bug, and what shape it is&lt;/strong&gt;. Automation is what turns a coverage &lt;em&gt;snapshot&lt;/em&gt; into a coverage &lt;em&gt;invariant&lt;/em&gt; (re-asserted every commit, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b ∉ D&lt;/code&gt; keeps holding as trunk drifts). Fast, elegant, component-ish tests pin the behaviour cheaply and run every commit; slow full-stack tests pin it expensively and tend to get relegated to a non-continuous build, so they decay almost like the manual check does. So the real question - “do I need a second &lt;em&gt;manual&lt;/em&gt; QA pass on this fix?” - splits six ways:&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Automated coverage for &lt;em&gt;this&lt;/em&gt; fix&lt;/th&gt;
      &lt;th&gt;Shape&lt;/th&gt;
      &lt;th&gt;Second manual QA pass?&lt;/th&gt;
      &lt;th&gt;Why&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;full-stack / slow (or n/a)&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Yes - it’s the only check&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Nothing pins the behaviour. Manual is your sole signal, on both branches. Maximum exposure to drift.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;fast / component-ish&lt;/td&gt;
      &lt;td&gt;&lt;em&gt;n/a&lt;/em&gt;&lt;/td&gt;
      &lt;td&gt;If there were fast component tests, you’d &lt;em&gt;have&lt;/em&gt; automation - this row doesn’t exist.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Some&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;full-stack / slow&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Yes, but scoped&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;A slow suite that may not run per-commit; manual still earns its keep for the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q \ D&lt;/code&gt; crescent (environment-specific behaviour).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Some&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;fast / component-ish&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Rarely&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;The fast tests already re-assert the core behaviour every commit; manual only justified for genuinely environment-only effects.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Good&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;full-stack / slow&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Redundant-ish&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Behaviour is pinned, but if the suite isn’t continuous you still inherit some decay - a thin manual smoke is defensible, not required.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Good&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;fast / component-ish&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Redundant&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;b ∉ D&lt;/code&gt; is re-proven on every commit and the cheap test &lt;em&gt;is&lt;/em&gt; the invariant. The second manual pass adds no bits. This is the case where my “not needed” is flatly true.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Read top-to-bottom, the manual second pass goes from &lt;em&gt;load-bearing&lt;/em&gt; to &lt;em&gt;theatre&lt;/em&gt; exactly as automation deepens &lt;strong&gt;and&lt;/strong&gt; gets faster/component-ish. The diagonal is the point: it’s not enough to have “good” tests - slow good tests that don’t run continuously still let coverage decay between the desk-check and release day. Fast, elegant, component-level tests are what actually collapse &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Q \ D&lt;/code&gt; toward empty and keep it there, which is the only regime where double-testing the fix is genuinely wasted motion. So when I say the double testing isn’t needed, the honest scope is &lt;strong&gt;the bottom row&lt;/strong&gt; - and the engineering move for everyone above it is to climb toward that row, not to keep paying for the manual pass.&lt;/p&gt;

&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;

&lt;p&gt;So, was I right to tell my friend it’s statistically impossible? Yes - with the precision that makes it true rather than glib. The &lt;strong&gt;fixed bug&lt;/strong&gt; cannot be the thing still broken on trunk after the release-branch QA pass, because trunk holds the same fix bytes and the same bytes can’t pass quality assurance there and fail here. What &lt;em&gt;remains&lt;/em&gt; is a sub-thousandth-to-sub-hundred-thousandth chance, and it isn’t the bug: it’s &lt;strong&gt;P(QA was wrong) + P(trunk became a different codebase since the fix)&lt;/strong&gt;. Drift and bad QA - not resurrection.&lt;/p&gt;

&lt;p&gt;The cheap moves that drain even that residual: &lt;strong&gt;leave a regression test behind in trunk’s CI&lt;/strong&gt; (this one always pays, for everyone), and - &lt;em&gt;if your cherry-picks aren’t reliably seamless&lt;/em&gt; - &lt;strong&gt;dry-run the cherry-pick early so a clash informs you instead of ambushing you.&lt;/strong&gt; The dry-run is for the hard-drift shops; for a tight-loop team whose cherry-picks just apply, the regression test is the move that matters and the fast same-day cherry-pick already does the rest. The dry-run, where it’s warranted, turns silent divergence into a visible signal before nerves are involved. And the regression test pins the fix for good: a human desk-check is true at one commit; a new unit test is true at every commit after it. Branch-for-release with one-way cherry-picks is a fine model precisely because the fix is &lt;em&gt;already in trunk&lt;/em&gt; - the work left over is verification, and verification is cheap to automate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final question for the reader to answer:&lt;/strong&gt; Change the full QA team sign off to the trunk (CI into “shared dev” say), and don’t do even the desk check in the release environment after the very procedural cherry pick … what’s the 1 in N failure rate?&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>The limits of merging experiment</title>
   <link href="https://paulhammant.com/2026/04/28/limits-of-merging-experiment/"/>
   <updated>2026-04-28T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/04/28/limits-of-merging-experiment</id>
   <content type="html">&lt;p&gt;A colleague suggested the cherry-picking way for trunk-based-development with branch for release needs worked examples cos there are foot-guns.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Cherry-pick to a release branch isn’t crystal clear as a workflow&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The failure modes are more interesting than they look. To pick at it, I built a small playground - a single-file Sinatra CRUD app, an end-to-end Playwright test, 
and a series of trunk commits that would let me run the same scenario two different ways and see what git actually does.&lt;/p&gt;

&lt;p&gt;The repo is here: &lt;a href=&quot;https://github.com/paul-hammant/limits-of-merging-experiment&quot;&gt;paul-hammant/limits-of-merging-experiment&lt;/a&gt;. 
Clone it, run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./start.sh&lt;/code&gt; to make &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;solution/&lt;/code&gt; folder the git folder not the one you cloned.&lt;/p&gt;

&lt;p&gt;The run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle install&lt;/code&gt; to go get deps.&lt;/p&gt;

&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;

&lt;p&gt;Folder &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;solution/&lt;/code&gt; contains the ruby/sinatra app and a playwright test for it. It is three tier - html with JS, a ruby middle tier, and a sqlite base tier.  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reset.sh&lt;/code&gt; will
keep getting us back to a starting point. See “C1” below.&lt;/p&gt;

&lt;p&gt;Five hypothetical trunk commits, all reachable as patches in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches/&lt;/code&gt; and re-applicable on demand, are key to this experiment.&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Change&lt;/th&gt;
      &lt;th&gt;Touches&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;C1&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Initial Person CRUD app, seeded Flintstones, Playwright happy-path test&lt;/td&gt;
      &lt;td&gt;everything&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;C2&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; (string) - dropdown, JS validation, DB &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CHECK&lt;/code&gt; constraint&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.rb&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;happy_path_test.rb&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;C3&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Button text change to UPPERCASE: NEW PERSON / EDIT / DELETE / SAVE / CANCEL&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.rb&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;happy_path_test.rb&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;C4&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;INTEGER&lt;/code&gt; (1..6); dropdown values are now ints&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.rb&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;happy_path_test.rb&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;C5&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Add maintainer comment in header (cosmetic, previously untouched region)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app.rb&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The shape that matters for this experiment: &lt;strong&gt;C3 is cosmetic and unrelated to hair colour. C4 builds on C2. C5 is in an untouched corner.&lt;/strong&gt; This is normal trunk 
life - small unrelated changes interleaving in the same files.&lt;/p&gt;

&lt;p&gt;The hypothetical release branch is cut from &lt;strong&gt;C2&lt;/strong&gt;. The team wishes that was it for the release, and continues on an unfrozen trunk as normal. Later there’s something
that agreed as a bug fix (definately not feature creep) and it should be cherry picked to the release branch by the responsible “merge meister” or release engineer.
We want to ship bugfix C4 (the int conversion) but not C3 feature change (the uppercase buttons).&lt;/p&gt;

&lt;h2 id=&quot;scenario-1-git-am-is-honest-and-thats-why-it-fails&quot;&gt;Scenario 1: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git am&lt;/code&gt; is honest, and that’s why it fails&lt;/h2&gt;

&lt;p&gt;First release engineer instinct: apply the C4 patch directly to “stable” release branch.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ git checkout -b release c2
$ git am patches/c4-hair-color-int.patch
Applying: C4: hair_color stored as INTEGER (1..6), dropdown values become ints
error: patch failed: app.rb:133
error: app.rb: patch does not apply
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Loud failure. Why? Look at the failing hunk:&lt;/p&gt;

&lt;div class=&quot;language-diff highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;       &amp;lt;td&amp;gt;&amp;lt;%= h p[&apos;dob&apos;] %&amp;gt;&amp;lt;/td&amp;gt;
&lt;span class=&quot;gd&quot;&gt;-      &amp;lt;td&amp;gt;&amp;lt;%= h p[&apos;hair_color&apos;] %&amp;gt;&amp;lt;/td&amp;gt;
&lt;/span&gt;&lt;span class=&quot;gi&quot;&gt;+      &amp;lt;td&amp;gt;&amp;lt;%= h(HAIR_COLORS[p[&apos;hair_color&apos;]]) %&amp;gt;&amp;lt;/td&amp;gt;
&lt;/span&gt;       &amp;lt;td class=&quot;row-actions&quot;&amp;gt;
         &amp;lt;a class=&quot;button&quot; href=&quot;/people/&amp;lt;%= p[&apos;id&apos;] %&amp;gt;/edit&quot;&amp;gt;EDIT&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The patch’s &lt;em&gt;context lines&lt;/em&gt; (the unchanged &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EDIT&lt;/code&gt; reference &lt;strong&gt;around&lt;/strong&gt; the change) include C3’s uppercase text. On the release branch the buttons still say &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Edit&lt;/code&gt;. Context doesn’t match. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git am&lt;/code&gt; is strict about context - it refuses.&lt;/p&gt;

&lt;p&gt;This is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git am&lt;/code&gt; correctly doing its job. It’s not the failure mode I’m interested in.&lt;/p&gt;

&lt;h2 id=&quot;scenario-2-git-cherry-pick-is-helpful-and-thats-why-it-lies&quot;&gt;Scenario 2: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry-pick&lt;/code&gt; is helpful, and that’s why it lies&lt;/h2&gt;

&lt;p&gt;(we do a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./reset.sh&lt;/code&gt; to go back to the starting position)&lt;/p&gt;

&lt;p&gt;Second instinct, and what most developers actually type:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ git checkout -b release c2
$ git cherry-pick c4
Auto-merging app.rb
[release ...] C4: hair_color stored as INTEGER (1..6), dropdown values become ints
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Clean. No conflict. Test passes.&lt;/p&gt;

&lt;p&gt;Why? Because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry-pick&lt;/code&gt; doesn’t apply patches by context - it does a three-way merge with the parent of C4 as the merge base. The merger sees:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;merge base (C3 on trunk) had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EDIT&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;the cherry-pick target (release branch) has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Edit&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;C4 didn’t change either of those lines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So three-way merge correctly concludes “leave the case alone, just apply the hair-colour change.” Release ends up with C4’s diff cleanly applied on top of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Edit&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That’s the textbook good outcome. &lt;strong&gt;And it’s exactly the failure mode the colleague was warning about&lt;/strong&gt; - not because &lt;em&gt;this&lt;/em&gt; cherry-pick was wrong, but because the 
success was contingent on a property of the diffs that nobody checked. C3 happened to touch only button text. If C3 had ever-so-slightly tidied up the dropdown the 
cherry pick may have conflicted - forcing a human to arbitrate on it.&lt;/p&gt;

&lt;p&gt;The lies are little lies, from good intentions, perhaps.&lt;/p&gt;

&lt;h2 id=&quot;what-about-merge-point-tracking&quot;&gt;What about merge-point tracking?&lt;/h2&gt;

&lt;p&gt;Here’s where Subversion fans get nostalgic. SVN’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; tries to record on the release branch “I have integrated revisions r3, r4 from trunk.” 
A subsequent sweep merge of trunk into release knows to skip those.  Not just Subversion, but the “bigger” VCS technologies Perforce and Microsoft’s TFVC.&lt;/p&gt;

&lt;p&gt;Git has none of that. A cherry-pick produces a commit with a different SHA from the original - &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cat-file -p&lt;/code&gt; on the two reveals different parents, 
different trees, different hashes. The only audit trail is the optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(cherry picked from commit ...)&lt;/code&gt; line that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry-pick -x&lt;/code&gt; leaves in the 
commit message, and &lt;strong&gt;git itself does not consult that line for anything&lt;/strong&gt;. It’s a comment.&lt;/p&gt;

&lt;p&gt;So if you later merge trunk wholly into release, git’s merge base is the last common ancestor - which is &lt;strong&gt;C2&lt;/strong&gt;, not C4. 
Git will re-apply C3, C4, and C5 from trunk on top of release. If the cherry-picked C4 on release is byte-identical to trunk’s C4, the merger deduplicates 
silently. If they differ even slightly (a hotfix on release, a typo correction), you get spurious conflicts that look very real, with no way for git to say 
“you already integrated this, just take trunk’s copy.” Coming from Svn, Perforce or TFVC merge-point-tracking is not as you remember it. In a TBD + release branches workflow you would &lt;strong&gt;never&lt;/strong&gt; do a sweeping commit from trunk to the release branch - you would only do cherry picks and less and less so over time to that release branch. 
At some point the release branch has been superceded and eleigible for deletion.&lt;/p&gt;

&lt;p&gt;SVN’s mergeinfo aimed at this and got bitten by edge cases - subtree mergeinfo creep, properties getting out of sync if you bypass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn merge&lt;/code&gt;. The 
“slightly broken” reputation is earned. But the &lt;em&gt;intent&lt;/em&gt; - cross-branch awareness of what’s been integrated - is something git deliberately doesn’t have. 
Linus rejected it on simplicity grounds. The price is paid by anyone running long-lived release branches.&lt;/p&gt;

&lt;h2 id=&quot;the-order-of-cherry-picks-question&quot;&gt;The order of cherry-picks question&lt;/h2&gt;

&lt;p&gt;If C3 and C4 are eventually both wanted on the release branch, does the order matter? Two scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A - out of order.&lt;/strong&gt; Cherry-pick C4 first (the urgent fix), then C3 later (because someone decided uppercase buttons should ship after all):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;release branch cherry-picks: C2 then C4&apos; then C3&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Scenario B - in order.&lt;/strong&gt; Cherry-pick in trunk order:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;release branch cherry-picks: C2 then C3&apos; then  C4&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In both cases, trunk has C2, C3, and C4. We could then sweep-merge c5 from trunk into the release branch.&lt;/p&gt;

&lt;p&gt;Resulting tree hashes (with author/date/identity pinned so SHAs are deterministic):&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Tree hash after sweep merge&lt;/th&gt;
      &lt;th&gt;Merge commit SHA&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Scenario A (out of order)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;088b679...&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e96a8e0...&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Scenario B (in order)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;088b679...&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4ad3efc...&lt;/code&gt; (fast-forward!)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Tree hashes match. Content is byte-identical.&lt;/strong&gt; Empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; between the two release branches.&lt;/p&gt;

&lt;p&gt;But the commit graphs are different shapes. Scenario A produced a real merge commit with two parents - the cherry-picked C4’/C3’ on release and the trunk C5 - 
because the histories diverged. Scenario B’s cherry-picks produced commits byte-identical to trunk’s (same diffs, same pinned timestamps), so the sweep merge 
fast-forwarded; release’s tip &lt;em&gt;is&lt;/em&gt; main’s tip.&lt;/p&gt;

&lt;p&gt;So the answer to “does order matter” is layered:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Content:&lt;/strong&gt; no, both converge to the same source tree.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;History shape:&lt;/strong&gt; yes, you get a merge node in one and a flat history in the other.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;SHA equivalence:&lt;/strong&gt; no, never - parent chains differ, so commit SHAs cascade differently. SHA equality was never the right test for “did this work.”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;blocking-a-commit-scenario-c&quot;&gt;Blocking a commit (Scenario C)&lt;/h2&gt;

&lt;p&gt;The order question above is about C3 and C4 &lt;em&gt;both&lt;/em&gt; eventually shipping. What about C3 &lt;em&gt;not&lt;/em&gt; shipping at all — a hard “no, this isn’t for this release”? Cut release from C1 (so C2 also becomes a cherry-pick), cherry-pick C2 and C4, then:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ git merge -s ours --no-edit \
    -m &quot;block C3: merge -s ours (record without applying diff)&quot; c3
$ git merge --no-edit main                      # sweep
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git merge -s ours c3&lt;/code&gt; makes c3 a &lt;em&gt;parent&lt;/em&gt; of release via a merge commit, but the resulting tree is exactly “ours” — none of c3’s diff is applied. The merge-base machinery on subsequent merges then sees c3 as already integrated and skips it. It’s the structural equivalent of SVN’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--record-only&lt;/code&gt; and Perforce’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resolve -ay&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The scenario script probes “is each trunk tag reachable from release?” after every step, since reachability &lt;em&gt;is&lt;/em&gt; git’s audit trail:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;after Cherry-pick C2:       c2 yes, c3 no,  c4 no,  c5 no
after Cherry-pick C4:       c2 yes, c3 no,  c4 no,  c5 no
after -s ours block of C3:  c2 yes, c3 YES, c4 no,  c5 no   ← merge node makes c3 an ancestor
after sweep:                c2 yes, c3 yes, c4 yes, c5 yes  ← main is now an ancestor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c3&lt;/code&gt; flip on the third row is the block being recorded. After the sweep, all of main’s tags are reachable through the merge edge — but &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff release main&lt;/code&gt; reports only C3’s button-text changes (UPPERCASE on main, mixed-case on release). The block held.&lt;/p&gt;

&lt;p&gt;(Aside: c2 says “yes” right after its cherry-pick because the experiment pins author/date/identity, so the cherry-pick reproduced c2’s original SHA byte-for-byte. In a real workflow timestamps differ on every cherry-pick, so c2 would show “no” too — cherry-picks leave no DAG fingerprint, only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-s ours&lt;/code&gt; does.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing to notice&lt;/strong&gt;: git &lt;em&gt;can&lt;/em&gt; block a commit durably, but the audit shape is different from SVN/P4. Where SVN updates a property string and P4 writes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt; integration record, git records the decision as a &lt;em&gt;graph fact&lt;/em&gt; — a merge node with the blocked commit as a parent. Reading the audit trail later means inspecting the DAG and the commit message at the block step. There’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git mergeinfo&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git integrated&lt;/code&gt;. The data lives in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git log --merges&lt;/code&gt;, and the meaning lives in the message you typed.&lt;/p&gt;

&lt;h2 id=&quot;what-the-sha-actually-proves&quot;&gt;What the SHA actually proves&lt;/h2&gt;

&lt;p&gt;This is the bit I had to stop and think about. I’d been comparing commit SHAs and getting confused by the differences. The right framing:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Commit SHA equality means “byte-identical commit object including parents.” Tree hash equality means “byte-identical content.” Cherry-picks change parents. They cannot preserve commit SHAs. They can preserve tree hashes - and that’s the only equivalence that matters for correctness.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So when judging whether a cherry-pick + sweep merge produced the right result: &lt;strong&gt;diff the trees, not the commits&lt;/strong&gt;. A clean &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff main release&lt;/code&gt; after the sweep is the 
only proof you need.&lt;/p&gt;

&lt;h2 id=&quot;what-git-cant-tell-you&quot;&gt;What git can’t tell you&lt;/h2&gt;

&lt;p&gt;Putting it together, here is what git silently &lt;em&gt;cannot&lt;/em&gt; answer for a release branch built from cherry-picks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;“Have I already integrated this trunk commit?”&lt;/strong&gt; Git’s answer is “no” - even if you cherry-picked it. The DAG has no link after the even (ignoring formatted comments).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“When I sweep merge runk to release, will it be a no-op?”&lt;/strong&gt; Only if every cherry-picked patch is byte-identical to its trunk twin. There’s no machine check.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Are these two release branches functionally equivalent?”&lt;/strong&gt; Only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; of trees can tell you. Commit history is misleading.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Did this cherry-pick land safely?”&lt;/strong&gt; Only your automated tests can tell you. Three-way merge succeeding is necessary, not sufficient.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;the-real-risk-in-real-corporate-codebases&quot;&gt;The real risk in real corporate codebases&lt;/h2&gt;

&lt;p&gt;A common pushback: “in a million-line corporate codebase, two unrelated commits almost never touch the same file region - cherry-picks land cleanly the vast majority of the 
time.” That’s empirically true. The base rate of textual collision is low.&lt;/p&gt;

&lt;p&gt;But the question isn’t frequency, it’s &lt;strong&gt;severity when it does happen&lt;/strong&gt;. The classic bad outcome isn’t a noisy &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git am&lt;/code&gt; rejection - it’s a quiet &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git cherry-pick&lt;/code&gt; 
that three-way-merges into wrong-but-plausible code, ships to a release branch, passes your tests because the tests don’t cover the exact corner that broke, 
and surfaces in production a week later. Git gives you no warning. There’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git fsck --semantic&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The mitigations I keep coming back to:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Work in thin vertical slices.&lt;/strong&gt; This is a good idea generally, but it especially pays off when cherry-picks are in your future. A single commit/PR that changes the DB schema, the middle tier, and the UI together is one cherry-pick - either it all lands on the release branch or none of it does. Split the same bug fix across three commits (one per tier) and you now have three cherry-picks that each have to be remembered, ordered, and applied. Miss one and you ship a half-fix; the release branch compiles, the smoke test passes, and the bug is “fixed” everywhere except the layer you forgot. Your pre-commit automated tests on the release branch should catch the omission - but “should” is doing heavy lifting there, and the failure mode is exactly the kind of partial-state subtlety tests are weakest at.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test the release branch like it’s a fresh codebase&lt;/strong&gt;, not “trunk minus a few commits.” End-to-end, not just the change you cherry-picked.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cherry-picks of schema/data changes need extra scrutiny.&lt;/strong&gt; Migration logic written assuming a trunk DB state may break against a release DB state.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Prefer release-from-trunk over release-with-cherry-picks&lt;/strong&gt; when your cadence allows it - roughly weekly or faster. If you ship every few days, a release branch buys you very little and the cherry-pick overhead isn’t worth it; just tag trunk. Cherry-picked release branches earn their keep at monthly/quarterly cadences where the stabilization window is long enough that trunk has moved on substantially. Long-lived release branches that absorb selective fixes are a structural risk, not a tooling problem git is going to grow out of.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;For high-cost cherry-picks, run the full test suite on the cherry-picked branch before merge.&lt;/strong&gt; This is what the playground demonstrates: a happy-path Selenium/Cypress/Playwright test that hits the UI and asserts on the DB shows up regressions that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff&lt;/code&gt; won’t.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;reproducing-this&quot;&gt;Reproducing this&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/paul-hammant/limits-of-merging-experiment
cd limits-of-merging-experiment
./start.sh                          # set up solution/ as a fresh playground
./scenario-a-out-of-order.sh        # release: C2, C4, C3, then merge main
./rollback.sh
./scenario-b-in-order.sh            # release: C2, C3, C4, then merge main
./rollback.sh
./scenario-c-block-c3.sh            # release@C1: cherry-pick C2 + C4, BLOCK C3, sweep main
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each script prints the resulting graph, tree hash, and diff. Compare the two.&lt;/p&gt;

&lt;p&gt;The patches in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches/&lt;/code&gt; are real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git format-patch&lt;/code&gt; output - readable, replayable, and the source of truth for the trunk timeline. The scenario scripts pin author identity and timestamps so anyone running them gets the same SHAs I did. That’s the only way to make a cherry-pick experiment reproducible.&lt;/p&gt;

&lt;h2 id=&quot;repeating-in-svn-what-svnmergeinfo-actually-looks-like&quot;&gt;Repeating in SVN: what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; actually looks like&lt;/h2&gt;

&lt;p&gt;Earlier I waved at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; as the thing git deliberately doesn’t have. Worth showing the property string itself, because the shape is the whole point.&lt;/p&gt;

&lt;p&gt;The same C2–C5 timeline replayed in a fresh local SVN repo gives this revision map:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Repo rev&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;r1&lt;/td&gt;
      &lt;td&gt;layout (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mkdir trunk + branches + tags&lt;/code&gt;)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r2&lt;/td&gt;
      &lt;td&gt;C1 — initial Person CRUD app&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r3&lt;/td&gt;
      &lt;td&gt;C2 — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; (string)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r4&lt;/td&gt;
      &lt;td&gt;C3 — UPPERCASE buttons&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r5&lt;/td&gt;
      &lt;td&gt;C4 — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; INTEGER&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r6&lt;/td&gt;
      &lt;td&gt;C5 — maintainer comment&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;r7&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn copy /trunk@r3 /branches/release&lt;/code&gt; (cut at C2)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;scenario-a--cherry-pick-out-of-order-c4-then-c3-then-sweep&quot;&gt;Scenario A — cherry-pick out of order (C4 then C3, then sweep)&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ cd svn-wc/branches/release

$ svn merge -c5 ^/trunk .            # cherry-pick C4
$ svn commit -m &quot;cherry-pick C4 from trunk@r5&quot;
$ svn propget svn:mergeinfo .
  /trunk:5

$ svn merge -c4 ^/trunk .            # cherry-pick C3
$ svn commit -m &quot;cherry-pick C3 from trunk@r4&quot;
$ svn propget svn:mergeinfo .
  /trunk:4-5

$ svn merge ^/trunk .                # sweep
--- Merging r6 through r9 into &apos;.&apos;:
U    app.rb
$ svn commit -m &quot;sweep merge ^/trunk into release&quot;
$ svn propget svn:mergeinfo .
  /trunk:4-9
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;scenario-b--cherry-pick-in-trunk-order-c3-then-c4-then-sweep&quot;&gt;Scenario B — cherry-pick in trunk order (C3 then C4, then sweep)&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ svn merge -c4 ^/trunk .            # cherry-pick C3
$ svn commit -m &quot;cherry-pick C3 from trunk@r4&quot;
$ svn propget svn:mergeinfo .
  /trunk:4

$ svn merge -c5 ^/trunk .            # cherry-pick C4
$ svn commit -m &quot;cherry-pick C4 from trunk@r5&quot;
$ svn propget svn:mergeinfo .
  /trunk:4-5

$ svn merge ^/trunk .                # sweep
$ svn commit -m &quot;sweep merge ^/trunk into release&quot;
$ svn propget svn:mergeinfo .
  /trunk:4-9
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;what-the-property-is-telling-you&quot;&gt;What the property is telling you&lt;/h3&gt;

&lt;p&gt;Both scenarios converge to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-9&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn diff ^/trunk ^/branches/release&lt;/code&gt; reports no file content difference — only this property exists on the branch root. The intermediate path differs (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:5&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-5&lt;/code&gt; versus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-5&lt;/code&gt;), but order doesn’t matter to the end state, just like in git.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; different from git: the sweep &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn merge ^/trunk&lt;/code&gt; reads the property and refuses to re-apply revisions named in it. Only &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r6&lt;/code&gt; (C5) actually produced edits in the sweep — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r4&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r5&lt;/code&gt; were already accounted for. SVN can answer the question “have I integrated this trunk revision yet?” because it wrote down the answer the first time. Git cannot, because git deliberately wrote nothing down.&lt;/p&gt;

&lt;p&gt;There’s a quirk visible in the final string: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-9&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-6&lt;/code&gt;. SVN records the &lt;em&gt;closed range it considered&lt;/em&gt; during the sweep, including repo revisions that touched neither trunk nor any merge source — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r7&lt;/code&gt; was the branch copy, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r8&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r9&lt;/code&gt; were the cherry-pick commits themselves. This is exactly the kind of “mergeinfo creep” SVN earned its slightly-broken reputation for. It’s harmless here; it can become noisy across years of long-lived branches, particularly if anyone bypasses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn merge&lt;/code&gt; and edits properties by hand.&lt;/p&gt;

&lt;h3 id=&quot;scenario-c--blocking-c3-with---record-only&quot;&gt;Scenario C — blocking C3 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--record-only&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;The third question worth asking: what about the change you &lt;em&gt;don’t&lt;/em&gt; want to ship? Suppose the team’s verdict on C3 (UPPERCASE buttons) is “not for this release” — not “we’ll cherry-pick it later” but a hard no. Re-cut the release branch from C1 (so C2 also becomes a cherry-pick), then:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ svn merge -c3 ^/trunk .                # cherry-pick C2 (the wanted feature)
$ svn merge -c5 ^/trunk .                # cherry-pick C4 (bugfix on top of C2)
$ svn merge --record-only -c4 ^/trunk .  # block C3: record but do not apply
$ svn merge ^/trunk .                    # sweep — should only pick up C5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--record-only&lt;/code&gt; flag adds the revision to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; &lt;em&gt;without applying its diff&lt;/em&gt;. The property evolution:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;after cherry-pick C2 (-c3):       /trunk:3
after cherry-pick C4 (-c5):       /trunk:3,5      ← non-contiguous, r4 absent
after record-only block (-c4):    /trunk:3-5      ← r4 fills in, no diff applied
after sweep:                      /trunk:3-10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The sweep consults the property, sees r4 is accounted for, and skips it. The final release tree differs from trunk only by C3’s button-text change — the block held.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing to notice&lt;/strong&gt;: the post-block property string is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:3-5&lt;/code&gt;. That is &lt;em&gt;exactly&lt;/em&gt; what the string would say if C3 had been merged normally. It cannot tell you whether r4 was applied or blocked — only that it was &lt;em&gt;considered&lt;/em&gt;. SVN’s mergeinfo records “we accounted for this revision,” not the intent behind that accounting. Future-you reading the property in a year has to fall back on the commit message at the block step. The data structure remembers the bookkeeping but not the decision.&lt;/p&gt;

&lt;h3 id=&quot;reproducing-the-svn-side&quot;&gt;Reproducing the SVN side&lt;/h3&gt;

&lt;p&gt;The scripts are on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn-version&lt;/code&gt; branch of the same repo:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git checkout svn-version
sudo apt install subversion       # or your platform&apos;s equivalent
./start.sh                        # build trunk r2..r6 + release@r3
./scenario-a-out-of-order.sh      # cherry-pick C4 then C3, then sweep
./rollback.sh                     # wipe svn-repo/ and svn-wc/
./scenario-b-in-order.sh          # cherry-pick C3 then C4, then sweep
./rollback.sh
./scenario-c-block-c3.sh          # release@r2; cherry-pick C2 + C4, BLOCK C3, sweep
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each scenario script wipes and rebuilds the repo (SVN is append-only, so “reset to a past revision” means start over), prints the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; value after every step, and ends with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn diff&lt;/code&gt; of trunk against release. The same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patches/&lt;/code&gt; directory feeds both the git and SVN flows — the patches are applied with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch -p1&lt;/code&gt; rather than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git apply&lt;/code&gt;, because the SVN working copy lives inside the outer git worktree and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git apply&lt;/code&gt; would treat it as the parent repo’s index.&lt;/p&gt;

&lt;p&gt;So: SVN does have the audit trail, the property is human-readable, and the sweep merge is genuinely aware of it. The cost is the property’s tendency to grow ranges that include revisions it had no business including, plus the institutional discipline of never touching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; directly. Whether that’s a better trade than git’s “we keep no record at all” is a judgement call about what failure mode you’d rather face — false reassurance from a slightly-wrong record, or no record and a test suite doing all the work.&lt;/p&gt;

&lt;h2 id=&quot;repeating-in-perforce-integration-records-not-properties&quot;&gt;Repeating in Perforce: integration records, not properties&lt;/h2&gt;

&lt;p&gt;Perforce solves the same problem SVN does — &lt;em&gt;what’s been integrated where&lt;/em&gt; — but it stores the answer per-file in the integration database rather than as a string-property on the branch root. Run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4 integrated&lt;/code&gt; and the depot tells you, for every file revision on the branch, which trunk revision it came from and whether it arrived as a clean copy or a three-way merge.&lt;/p&gt;

&lt;p&gt;Same C2–C5 timeline, replayed against a local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4d&lt;/code&gt;:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Changelist&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;CL1&lt;/td&gt;
      &lt;td&gt;C1 — initial Person CRUD app&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CL2&lt;/td&gt;
      &lt;td&gt;C2 — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; (string)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CL3&lt;/td&gt;
      &lt;td&gt;C3 — UPPERCASE buttons&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CL4&lt;/td&gt;
      &lt;td&gt;C4 — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hair_color&lt;/code&gt; INTEGER&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CL5&lt;/td&gt;
      &lt;td&gt;C5 — maintainer comment&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CL6&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4 populate //depot/trunk/...@2 //depot/branches/release/...&lt;/code&gt; (cut at C2)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;scenario-a--cherry-pick-out-of-order-c4-then-c3-then-sweep-1&quot;&gt;Scenario A — cherry-pick out of order (C4 then C3, then sweep)&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ p4 integrate //depot/trunk/...@4,@4 //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/...
$ p4 submit -d &quot;cherry-pick C4 from trunk@CL4&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#2 - merge from //depot/trunk/app.rb#4

$ p4 integrate //depot/trunk/...@3,@3 //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/...
$ p4 submit -d &quot;cherry-pick C3 from trunk@CL3&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#3 - merge from //depot/trunk/app.rb#3
//depot/branches/release/app.rb#2 - merge from //depot/trunk/app.rb#4

$ p4 integrate //depot/trunk/... //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/...
$ p4 submit -d &quot;sweep merge //depot/trunk into //depot/branches/release&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#3 - merge from //depot/trunk/app.rb#3
//depot/branches/release/app.rb#2 - merge from //depot/trunk/app.rb#4
//depot/branches/release/app.rb#4 - copy from //depot/trunk/app.rb#5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;scenario-b--cherry-pick-in-trunk-order-c3-then-c4-then-sweep-1&quot;&gt;Scenario B — cherry-pick in trunk order (C3 then C4, then sweep)&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ p4 integrate //depot/trunk/...@3,@3 //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/... ; p4 submit -d &quot;cherry-pick C3&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#2 - copy from //depot/trunk/app.rb#3

$ p4 integrate //depot/trunk/...@4,@4 //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/... ; p4 submit -d &quot;cherry-pick C4&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#2 - copy from //depot/trunk/app.rb#3
//depot/branches/release/app.rb#3 - copy from //depot/trunk/app.rb#4

$ p4 integrate //depot/trunk/... //depot/branches/release/...
$ p4 resolve -am //depot/branches/release/... ; p4 submit -d &quot;sweep merge&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch from //depot/trunk/app.rb#1,#2
//depot/branches/release/app.rb#2 - copy from //depot/trunk/app.rb#3
//depot/branches/release/app.rb#3 - copy from //depot/trunk/app.rb#4
//depot/branches/release/app.rb#4 - copy from //depot/trunk/app.rb#5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;what-the-records-are-telling-you&quot;&gt;What the records are telling you&lt;/h3&gt;

&lt;p&gt;Same content in both scenarios — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4 diff2 //depot/trunk/... //depot/branches/release/...&lt;/code&gt; reports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;identical&lt;/code&gt; for every file. The two depots converged.&lt;/p&gt;

&lt;p&gt;But the integration verbs differ in a way SVN’s mergeinfo and git’s history don’t expose at all:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Scenario A&lt;/strong&gt;: every cherry-pick is recorded as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge from&lt;/code&gt;. Cherry-picking C4 onto a branch that doesn’t yet have C3 forced a three-way resolve under the hood — P4 noticed and labelled it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scenario B&lt;/strong&gt;: every cherry-pick is recorded as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy from&lt;/code&gt;. C3 then C4 in trunk order produced clean takes on each step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The “verb that landed me here” is part of the audit trail. If you ever investigate why a release branch file diverges from trunk, knowing whether it got there via a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt; or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt; (and from which exact source revision) is the question P4 answers and the question git can’t.&lt;/p&gt;

&lt;p&gt;The sweep &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4 integrate //depot/trunk/... //depot/branches/release/...&lt;/code&gt; with no rev range consults the integration database and only re-applies revisions that haven’t been credited yet — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r5&lt;/code&gt; (C5) in our run. That’s what P4 marketing meant by “merge tracking” decades before SVN tried to bolt the same idea on with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt;. The cost is that it’s all &lt;em&gt;server-side&lt;/em&gt; state, locked behind the depot — there’s no offline, no pull-request workflow, and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Unloaded depot&lt;/code&gt; for archival is its own ceremony. The benefit is the integration history is structured, queryable per-file, and never gets out of sync with what was actually integrated.&lt;/p&gt;

&lt;h3 id=&quot;scenario-c--blocking-c3-with-resolve--ay&quot;&gt;Scenario C — blocking C3 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resolve -ay&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;Same workflow as the SVN scenario C, branched from C1: cherry-pick C2, cherry-pick C4, block C3, sweep.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ p4 integrate //depot/trunk/...@3,@3 //depot/branches/release/...
$ p4 resolve -ay //depot/branches/release/...     # accept yours = keep target, ignore source
$ p4 submit -d &quot;block C3: integrate + accept-yours of trunk@CL3 (no diff)&quot;
$ p4 integrated //depot/branches/release/app.rb
//depot/branches/release/app.rb#1 - branch  from //depot/trunk/app.rb#1
//depot/branches/release/app.rb#2 - copy    from //depot/trunk/app.rb#2     (C2)
//depot/branches/release/app.rb#3 - merge   from //depot/trunk/app.rb#4     (C4)
//depot/branches/release/app.rb#4 - ignored //depot/trunk/app.rb#3          (C3 — blocked)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The integration verb is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt;. P4’s per-file integration database has three states for any source revision: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;branch&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt; (it landed) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt; (it was considered and rejected). After the sweep:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;//depot/branches/release/app.rb#5 - merge   from //depot/trunk/app.rb#5     (C5)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;C3 stays &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt;. The block held, and the audit trail says, in machine-readable form, &lt;em&gt;what happened&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing to notice&lt;/strong&gt;: this is the cleanest case where P4 carries strictly more information than SVN. SVN’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; records &lt;em&gt;that&lt;/em&gt; a revision was accounted for; P4’s integration database records &lt;em&gt;whether the bytes were taken or rejected&lt;/em&gt;. Asked “did anyone consider C3 for this release?” the SVN string says yes; the P4 record says yes &lt;em&gt;and&lt;/em&gt; tells you the answer was “no, ignored.”&lt;/p&gt;

&lt;h3 id=&quot;reproducing-the-perforce-side&quot;&gt;Reproducing the Perforce side&lt;/h3&gt;

&lt;p&gt;Scripts are on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perforce-version&lt;/code&gt; branch:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git checkout perforce-version
# install p4 + p4d — see https://www.perforce.com/downloads/helix-core
# (or the primer at github.com/paul-hammant/fast_perforce_setup)
./start.sh                       # build trunk CL1..CL5 + release@CL2 on a sandbox p4d
./scenario-a-out-of-order.sh     # cherry-pick C4 then C3, then sweep
./rollback.sh                    # stop p4d, wipe p4-server/ and p4-wc/
./scenario-b-in-order.sh         # cherry-pick C3 then C4, then sweep
./rollback.sh
./scenario-c-block-c3.sh         # release@CL1; cherry-pick C2 + C4, BLOCK C3, sweep
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The sandbox runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4d&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:1667&lt;/code&gt; (not the conventional 1666), with no SSL and no security level set, so no passwords. Everything lives under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4-server&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p4-wc&lt;/code&gt;; both are wiped on every run. Patches are applied with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;patch -p1&lt;/code&gt; for the same reason as the SVN scripts — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git apply&lt;/code&gt; would notice the outer git worktree and refuse to write.&lt;/p&gt;

&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;

&lt;h3 id=&quot;so-which-vcs-knows-whats-been-integrated&quot;&gt;So which VCS “knows what’s been integrated”?&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;Records integration history?&lt;/th&gt;
      &lt;th&gt;Where it lives&lt;/th&gt;
      &lt;th&gt;What it records&lt;/th&gt;
      &lt;th&gt;Distinguishes “applied” from “blocked”?&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Git&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;No&lt;/td&gt;
      &lt;td&gt;Nowhere (optional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(cherry picked from …)&lt;/code&gt; comment, never read)&lt;/td&gt;
      &lt;td&gt;Nothing machine-checkable&lt;/td&gt;
      &lt;td&gt;No&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;SVN&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;svn:mergeinfo&lt;/code&gt; property on branch root&lt;/td&gt;
      &lt;td&gt;A revision-range string, e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/trunk:4-9&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No — both look the same in mergeinfo&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Perforce&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
      &lt;td&gt;Per-file integration database&lt;/td&gt;
      &lt;td&gt;Source path, source rev, integration verb (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;branch&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;copy&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;merge&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Yes — the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ignored&lt;/code&gt; verb&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;All three converge on the same source tree when the underlying patches don’t conflict. The difference is what the tool can &lt;em&gt;tell you afterwards&lt;/em&gt; about how that tree got built — and therefore what kinds of “did the cherry-pick land safely?” questions you can ask the tool versus answer with tests.&lt;/p&gt;

&lt;p&gt;Cherry-pick is not infallible. It also isn’t usually wrong. The hazard is the gap between those two facts: &lt;strong&gt;git’s vocabulary for “this is the same change” is “this is byte-identical bytes,” and outside of that narrow case, it has no opinion&lt;/strong&gt;. SVN tried to fill that gap and is imperfect in it implementation. Git decided the mess wasn’t worth it.&lt;/p&gt;

&lt;p&gt;What this means in practice for trunk-based development with release branches: cherry-pick is a tool that requires you to bring your own audit. The audit is your 
test suite, your code review, and your CI. If those are weak, cherry-pick is a foot-gun. If they’re strong, cherry-pick is fine - and the SHA divergence between trunk and 
release is just bookkeeping noise.&lt;/p&gt;

&lt;p&gt;So per my colleagues nudge - cherry picks are great, but be careful and know the limits.&lt;/p&gt;

&lt;p&gt;One last thing: I’ve been in engineering leadership previously for a 12 planned releases a year team, and sometimes late features were merged to the release branch using 
cherry-pick. Don’t do that - wait for the next release. I lost the argument cos the business really really wanted those late features - more than once from the for the same 
component of the system. Chery pick is for release stabilizing things only: toggle off your work that isn’t ready to go live before the branch cut moment.&lt;/p&gt;

&lt;p&gt;Updates:&lt;/p&gt;

&lt;p&gt;May 7, 2026: add scenario-c. Also svn and perforce branches.&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Live Verify</title>
   <link href="https://paulhammant.com/2026/03/17/live-verify/"/>
   <updated>2026-03-17T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2026/03/17/live-verify</id>
   <content type="html">&lt;p&gt;Nobody cares about a thing in the tech world until it is successful, and I have something here that could be, but isn’t yet. Its a brand new idea and has an immense chasm to cross. Most of it is an adoption/patronage chasm, but there are technical challenges too.&lt;/p&gt;

&lt;p&gt;I’ve blogged before on this enough to have timed out any change of filing a patent on it. So there’s no way I could make a SaaS that
would have some exclusivity to operate based on patent. Not that patents really protect business ideas these days anyway.&lt;/p&gt;

&lt;h1 id=&quot;live-verify-links&quot;&gt;Live verify links&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;GitHub home page: &lt;a href=&quot;https://live-verify.github.io/live-verify/&quot;&gt;live-verify.github.io/live-verify/&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;GitHub Repo: &lt;a href=&quot;https://github.com/live-verify/live-verify&quot;&gt;github.com/live-verify/live-verify&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is me and ClaudeCode mostly. Me reminding Claude that I really really like automated tests.&lt;/p&gt;

&lt;p&gt;The home page has screenshots of live-verify in action (before, after verification, trust statement), and some real videos of tests running. Some of those videos are me showing camera apps, and some are voiceless screen recordings of a larger multi-tier simulation of all the tech pieces. The home page also links to 480 or so use cases (and has a search feature).&lt;/p&gt;

&lt;h1 id=&quot;what-is-it&quot;&gt;What is it?&lt;/h1&gt;

&lt;p&gt;A system to verify a document or part of a document in your hands that might be way away from the system that produced it. And verify means a claim within - could be just a rectangle of text mid way through the document. Could be 20 rectangles and 20 verifications. The could also be from something that’s printed and in front of you, using a camera-using phone app. Perhaps only for small rectangles of text. Most of the time you’ve the document as a digital document. Say a PDF or a web page. In that case the verification is via a Chrome extension. In time the same tech is built into Adobe, Outlook, MsWord etc. At some point it is in the OS and the need for a chrome extension disappears.&lt;/p&gt;

&lt;p&gt;There’s 480 use cases that are listed. Maybe that’s only 280 after consolidation.  Uses cases are anti-fraud, safety, and accountability. To say more, use cases cover preventing document fraud (fake certificates, forged receipts), verifying safety credentials (building inspectors, healthcare workers, equipment compliance), and enforcing accountability (audit trails, regulatory compliance, chain of custody). “Accountability” is a catch-all though. It captures the compliance, ethics, audit, and delegated-authority themes that don’t fit neatly under fraud or safety.&lt;/p&gt;

&lt;p&gt;Most of the time it is about reducing costs of things by rolling back fraud, because fraudulent claims would now be easier and quicker to identify and avoid the consequences of. Yes, “claim” in an insurance thing, but I’m talking about more general use like this claim “I, Jimmy Cricket, worked at Microsoft in Seattle from 2010 to 2013 in the DevOps team”, or “Mr Alex E Spooner earned his millions from his family’s corner shop and not from selling drugs at all. He would make a great investor”.  Other uses, could be ID systems with a camera app confirming the person with the eInk badge-ID is actually a police officer. That’d need rules of conduct for those verification moments.&lt;/p&gt;

&lt;p&gt;The system rests on Human eyes being quickly able to scan a claim, and wonder “I would trust this if only there was a way of verifying it as true”, then a button press and some maths suggesting it is true (is “verified”) and a chain of trust that human eyes can also quickly scan toward another trust or not trust decision.  And a system that’s much more trustworthy than presentation and use of QR code.&lt;/p&gt;

&lt;p&gt;No personal data ever leaves your device. The text is captured, normalized, and hashed entirely on-device. Only the hash — a one-way fingerprint that reveals nothing about the document — is sent over the network. The verification endpoint never sees your degree certificate, salary receipt, or passport. Tens of thousands of cryptography PhDs could testify in court that hashing is irreversible.&lt;/p&gt;

&lt;p&gt;The maths is hashing - SHA256 by default, but could be stronger or weaker. Some education needed for ordinary people to broadly understand why it is one-way. The hashes are all in public and not indexed. You know the hash, you can see the payload (default would be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{ &quot;status&quot;:&quot;verified&quot; }&lt;/code&gt;). You don’t know the hash, you can’t discover it, subject to the plain-text’s entropy.&lt;/p&gt;

&lt;p&gt;Revocation is built in. “Verified” isn’t permanent. An issuer can change the status to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revoked&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;suspended&lt;/code&gt;, or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expired&lt;/code&gt; at any time — just update what the endpoint returns. A doctor loses their license, a certificate is superseded, an employee is terminated: the next person who verifies sees the current status, not a stale “OK.” This is what static digital signatures can’t do.&lt;/p&gt;

&lt;p&gt;Trust rests on the domain, not the app. There’s no central authority or certificate registry. You decide whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed.ac.uk&lt;/code&gt; is an authority for Edinburgh degrees, or whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;coned.com&lt;/code&gt; is an authority for utility worker badges. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify:&lt;/code&gt; line on the document tells you whose domain is making the claim — and that domain can declare who authorized it. For example, in the automated tests there’s a &lt;strong&gt;James Whitfield bank statement&lt;/strong&gt; from &lt;strong&gt;Meridian National Bank&lt;/strong&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify:meridian-national.bank.us/statements&lt;/code&gt;. The extension verifies the hash, then walks the authority chain: Meridian National Bank says it’s authorized by the &lt;strong&gt;FDIC&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fdic.gov&lt;/code&gt;), which in turn is authorized by the &lt;strong&gt;US Treasury&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;treasury.gov&lt;/code&gt;). The verifier sees the full chain and decides whether to trust it.&lt;/p&gt;

&lt;p&gt;So we have &lt;strong&gt;two camera-using phone apps&lt;/strong&gt;, and a &lt;strong&gt;Chrome extension&lt;/strong&gt;. Ideally we’d go on to make some plugin for Acrobat and Outlook and more. The Chrome extension is working well, but the camera-apps have upper limits. One is size of “document” to be verified which is understandable as OCR though a camera lens isn’t perfect. The other is tabular data - read on for more on that.&lt;/p&gt;

&lt;p&gt;I wish there was more shared code, between the implementations:&lt;/p&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Platform&lt;/th&gt;
      &lt;th&gt;Normalization&lt;/th&gt;
      &lt;th&gt;Hashing&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Web app&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public/normalize.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;JavaScript (canonical)&lt;/td&gt;
      &lt;td&gt;Web Crypto API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Chrome extension&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shared/normalize.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;JavaScript (auto-generated copy via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/sync-shared.js&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Web Crypto API&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;iOS app&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;JavaScript via JSBridge (runs normalize.js directly)&lt;/td&gt;
      &lt;td&gt;CryptoKit (native Swift)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;Android app&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;JavaScript via Rhino (runs transpiled normalize.js)&lt;/td&gt;
      &lt;td&gt;Native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MessageDigest&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The web-app version was were I started with this some months ago. It used Tesseract OCR and would run online just asking for the camera. Tesseract (via WASM) was really problematic so I shifted to proper apps ahead of schedule.&lt;/p&gt;

&lt;p&gt;I also have a reference backend that we use in the built-in automated tests. The possibly hundreds of SaaS companies that supply into this space may make different choices. The value is the open standard here.&lt;/p&gt;

&lt;h2 id=&quot;post-verification-actions&quot;&gt;Post-Verification Actions&lt;/h2&gt;

&lt;p&gt;Verification doesn’t have to stop at “verified.” There are two sources of follow-up actions.&lt;/p&gt;

&lt;p&gt;The first is the &lt;strong&gt;app or phone itself&lt;/strong&gt;. If you’re scanning a coffee shop receipt and you have Expensify installed, the app can offer “Send to Expensify?” — that’s a client-side decision based on context, and a choice for you to accept or ignore. The receipt issuer doesn’t know or control what’s offered. The second is the &lt;strong&gt;verification endpoint&lt;/strong&gt; deliberately returning actions in its response. A building inspector’s badge could offer a form for the homeowner to record visit details (areas accessed, duration, any concerns). A lawyer’s credentials could link to the bar association’s public disciplinary record. These are issuer choices — the endpoint decides what to offer.&lt;/p&gt;

&lt;p&gt;The endpoint pattern scales from light (a link) to strong (a POST form for reporting). The strong version matters where there’s a power dynamic — an inspector at your door, a healthcare worker with a vulnerable patient. The verification response tells the verifier “you may record details of this interaction” and explicitly says they will never be told not to.&lt;/p&gt;

&lt;h2 id=&quot;current-tech-problems&quot;&gt;Current Tech Problems&lt;/h2&gt;

&lt;p&gt;Live Verify’s camera mode works beautifully for &lt;strong&gt;prose documents&lt;/strong&gt; — certificates, references, claims where text flows continuously line by line. Point your phone, tap verify, done.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;tabular data&lt;/strong&gt; — receipts, invoices, anything with left-aligned descriptions and right-aligned prices — breaks the pipeline. Many of the use cases involve tabular data.&lt;/p&gt;

&lt;h3 id=&quot;what-works-prose&quot;&gt;What Works: Prose&lt;/h3&gt;

&lt;p&gt;An employment reference like this verifies perfectly:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;I, Paul Hammant, worked for Kevin Behr in
his role as CIO of HedgeServ in New York City
in 2015 and 2016
verify:paulhammant.com/refs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The text flows left-to-right with no gaps. The OCR engine (Apple Vision on iOS, Google ML Kit on Android) sees one contiguous block of text. Verification succeeds.&lt;/p&gt;

&lt;h3 id=&quot;what-breaks-tabular-receipts&quot;&gt;What Breaks: Tabular Receipts&lt;/h3&gt;

&lt;p&gt;A coffee shop receipt like this fails:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Flat White                £3.40
Almond Croissant          £3.25
SUBTOTAL:                 £6.65
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/live_verify_snafu-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Where a human sees one line, the OCR engine sees two separate text blocks — “Flat White” and “£3.40” — because the visual gap signals “these are separate regions.” A receipt that should be one block becomes 10+ fragments. Both iOS and Android camera apps I have made attempting to use Live-Text features of the OS show up to 10 fragments. And that’s the best case sometimes there’s 7 and some crucial numbers on the right hand side are omitted completely.&lt;/p&gt;

&lt;h3 id=&quot;where-this-leaves-us&quot;&gt;Where This Leaves Us&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Document Type&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Clip Mode (Browser)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Camera (Android)&lt;/th&gt;
      &lt;th style=&quot;text-align: left&quot;&gt;Camera (iOS)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Short prose (peer references, badge claims)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Longer prose (certificates, full letters)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (OCR errors creep in)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (OCR errors creep in)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Tabular (receipts, invoices)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Works (stitching)&lt;/td&gt;
      &lt;td style=&quot;text-align: left&quot;&gt;Broken (single-rectangle)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Clip mode&lt;/strong&gt; — the browser extension — handles everything perfectly because it operates on digital text, not pixels. No OCR, no rectangle fragmentation.&lt;/p&gt;

&lt;h3 id=&quot;the-real-fix-registration-marks-for-tabular-data&quot;&gt;The Real Fix: Registration Marks for Tabular Data&lt;/h3&gt;

&lt;p&gt;Our stitching on Android is a workaround. It handles the common case but it’s fragile — font size changes, multi-column layouts, and unusual spacing can all defeat Y-coordinate grouping.&lt;/p&gt;

&lt;p&gt;The proper solution is for Apple and Google to support &lt;strong&gt;registration marks for tabular data&lt;/strong&gt; in their OCR APIs. Two marks at diagonal corners define a bounding rectangle. The OCR engine sees the marks, treats everything inside as one text block, and strips the marks from the output — like a QR finder pattern that the camera uses for orientation but doesn’t include in the payload.&lt;/p&gt;

&lt;p&gt;We already have one mark: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; line at the bottom-left of every verifiable region. A Unicode corner character — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; (U+231D, upper right corner) — on the first line at the right margin provides the opposing corner:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;8 Market Square                 ⌝
Henley-on-Thames RG9 2AA
Flat White                  £3.40
Almond Croissant            £3.25
SUBTOTAL:                   £6.65
vfy:r.the-daily-grind.co.uk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s already in the pic I took above.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; is a control mark, not content. The OCR engine consumes it for bounds detection and omits it from the text output, just as it would omit a QR finder square. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; line stays — it’s already part of the verification protocol and already in the OCR output.&lt;/p&gt;

&lt;p&gt;Why a Unicode character rather than an image or a special printed mark? Because it works everywhere text works: HTML, PDF, LaTeX, Word, thermal receipt printers. Any system that can render U+231D can print the mark. No image embedding, no special font, no binary format dependency.&lt;/p&gt;

&lt;p&gt;This is future work — it requires Apple and Google to recognize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;⌝&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vfy:&lt;/code&gt; pair as a single region boundary in their Vision and ML Kit frameworks. But the convention is simple, the marks are unobtrusive on the printed document, and the benefit is large: receipts, invoices, lab results, bank statements, and every other tabular document becomes verifiable by camera.&lt;/p&gt;

&lt;p&gt;Until then, camera apps work better for prose, and clip mode works for everything - including most of the anti-fraud cases.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Did You Send This - for module phone SMS/Voice</title>
   <link href="https://paulhammant.com/2025/11/30/did-you-send-this-challenge/"/>
   <updated>2025-11-30T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/11/30/did-you-send-this-challenge</id>
   <content type="html">&lt;p&gt;This article is a comprehensive 2025 update to the original 2006 post:
“&lt;a href=&quot;/blog/did-you-send-this.html&quot;&gt;Did you send this - another weapon against spam?&lt;/a&gt;”&lt;/p&gt;

&lt;h2 id=&quot;applying-dyst-to-mobile-communication&quot;&gt;Applying DYST to Mobile Communication&lt;/h2&gt;

&lt;p&gt;The original DYST idea could theoretically be adapted by Google and Apple,
who could implement it unilaterally for mobile calls and messages. Rather than
relying on telecommunication carriers, they could use their existing TCP/IP
infrastructure for verification. An app on the sending device would get a
temporary token from an Apple/Google server, and the receiving device would check
that token with the same central server before alerting the user.&lt;/p&gt;

&lt;h3 id=&quot;dyst-supporting-phone-or-ios-or-android&quot;&gt;DYST supporting phone or (iOS or Android)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a genuine call&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not sees&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone numb&amp;gt;&quot;&amp;lt;br&amp;gt;question. And handset&amp;lt;br&amp;gt;silently confirms she is
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If Bob is in the address book, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;bobs phone numb&amp;gt;&lt;/code&gt; is replaced with Bob’s name&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a genuine Apple/Google “Messages” (not SMS)&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;Bob: 1. Alice actually&amp;lt;br&amp;gt;sends text msg
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not see&amp;lt;br/&amp;gt;&quot;Did you send this&amp;lt;br&amp;gt;to &amp;lt;bob phone num&amp;gt;&quot;&amp;lt;br&amp;gt;question. And handset&amp;lt;br&amp;gt;silently confirms she did
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Apple launched the iPhone with iMessage (for other iPhone users). Google did the same for Android uses, and these 
days Rich Communication Services (RCS) allows both to interoperate. In the diagram above the teleco’s systems are not shown because they are
not involved for the routing of “smart” (non SMS) messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here is a sequence diagram illustrating the back-channel exchange for a fake call&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is fully DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge
    activate Alice
    Note over Alice: Alice does not see&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone num&amp;gt;&quot;&amp;lt;br&amp;gt;message. And handset&amp;lt;br&amp;gt;silently DENIES she is
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s auto&amp;lt;br&amp;gt;denial received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The sequence diagram for messages would look very similar.&lt;/p&gt;

&lt;h3 id=&quot;smartphone-supporting-rcs-but-not-yet-supporting-dyst-older-ios-or-android-version-perhaps&quot;&gt;Smartphone supporting RCS but not yet supporting DYST (older iOS or Android version perhaps)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legit call via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone numb&amp;gt;&quot;&amp;lt;br&amp;gt;question. And she&amp;lt;br&amp;gt;manually confirms.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Legit message via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;sends text msg
    AliceTeleco-&amp;gt;&amp;gt;Bob: Message signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Did you send this&amp;lt;br&amp;gt;to &amp;lt;bob phone num&amp;gt;&quot;&amp;lt;br&amp;gt;question. And she&amp;lt;br&amp;gt;manually confirms.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;confirmation received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Fake caller ID call via RCS (no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is RCS-enabled but not&amp;lt;br/&amp;gt;DYST-enabled.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (RCS)
    activate Alice
    Note over Alice: Alice sees interactive&amp;lt;br/&amp;gt;&quot;Are you placing a call&amp;lt;br&amp;gt;to &amp;lt;bobs phone num&amp;gt;&quot;&amp;lt;br&amp;gt;message. And she&amp;lt;br&amp;gt;manually DENIES.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s manual&amp;lt;br&amp;gt;denial received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;phone-not-supporting-rcs-nor-dyst-much-older-ios-or-android-version-perhaps&quot;&gt;Phone not supporting RCS nor DYST (much older iOS or Android version perhaps)&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Legit call via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;Initiates call
    AliceTeleco-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she&apos;s calling&amp;lt;br&amp;gt;&amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She replies &apos;YES&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;YES&apos; received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now rings or displays&amp;lt;br&amp;gt;message. Call/Text&amp;lt;br&amp;gt;connection established.&amp;lt;br/&amp;gt;Phone buzzes or rings
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Legit message via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant AliceTeleco as Alice&apos;s&amp;lt;br&amp;gt;Teleco
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Alice-&amp;gt;&amp;gt;AliceTeleco: 1. Alice actually&amp;lt;br&amp;gt;sends SMS msg
    AliceTeleco-&amp;gt;&amp;gt;Bob: SMS received
    activate Bob
    Note over Bob: Receives SMS,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she sent message to&amp;lt;br&amp;gt;&amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She replies &apos;YES&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Confirmed)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;YES&apos; received.
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Success
    deactivate Alliance
    activate Bob
    Note over Bob: Now displays message.&amp;lt;br/&amp;gt;Phone buzzes
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Fake caller ID call via SMS (no RCS, no DYST)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;sequenceDiagram
    participant Eve as Eve&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant EveVOIP as Eve&apos;s VoIP&amp;lt;br&amp;gt;Gateway
    participant Alice as Alice&apos;s Handset&amp;lt;br&amp;gt;(Originator)
    participant Bob as Bob&apos;s Handset&amp;lt;br&amp;gt;(Recipient)
    participant Alliance as Spam Alliance&amp;lt;br&amp;gt;Backend

    Eve-&amp;gt;&amp;gt;EveVOIP: 1. Eve actually&amp;lt;br&amp;gt;Initiates call&amp;lt;br&amp;gt;caller-id = Alice
    EveVOIP-&amp;gt;&amp;gt;Bob: Call Signalling
    activate Bob
    Note over Bob: Receives signalling,&amp;lt;br/&amp;gt;waits for verification.

    Bob-&amp;gt;&amp;gt;Alliance: 2. Verification Request
    deactivate Bob
    activate Alliance

    Note over Alliance: Determines Alice&apos;s handset&amp;lt;br/&amp;gt;is not RCS or DYST enabled.&amp;lt;br&amp;gt;Will use SMS.
    Alliance-&amp;gt;&amp;gt;Alice: 3. DYST Challenge (SMS)
    activate Alice
    Note over Alice: Alice sees SMS asking if&amp;lt;br/&amp;gt;she is calling &amp;lt;bobs phone num&amp;gt;.&amp;lt;br/&amp;gt;She ignores it or replies &apos;NO&apos;.
    Alice--&amp;gt;&amp;gt;Alliance: 4. Challenge Response (Denied)
    deactivate Alice
    Note over Alliance: Alice&apos;s SMS reply&amp;lt;br&amp;gt;of &apos;NO&apos; received (or timeout).
    Alliance-&amp;gt;&amp;gt;Bob: 5. Verification Failure
    deactivate Alliance
    activate Bob
    Note over Bob: Drops the call without&amp;lt;br&amp;gt;notifying Bob and doesnt&amp;lt;br&amp;gt;place it in recent calls
    deactivate Bob
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Of course a text reply of “No” would be inferred for anyting other that ‘Y’, y, YES, 是, 对, हाँ (haan), sí, oui, نعم, হ্যাঁ , da, sim, ہاں, ya, ja, はい, ndiyo, हो , అవును , evet, vâng / dạ&lt;/p&gt;

&lt;p&gt;This system could be enhanced with network intelligence. For IP-based messages,
the receiving service’s backend (e.g., Apple’s servers) could perform a traceroute
back to the sender’s IP. This wouldn’t reveal the user’s precise location but
would identify the network path. A path from a reputable mobile carrier like O2
in the UK would contribute to a “low-probability spammer” score, whereas a path
from a data center known for fraudulent activity would raise suspicion, all
without exposing the sender’s private location.&lt;/p&gt;

&lt;p&gt;A user could configure their device to “block all unverified calls”, “warn”, or
perform no checks. However, limitations of this solution would including the risk of blocking
legitimate calls if the verification service has an outage and the small delay
the check adds to every communication. To counter denial-of-service attacks
where bad actors flood users with fake verification messages to create fatigue
and distrust, the system would rely on server-side intelligence from the central
servers to detect and rate-limit anomalous floods of verification requests.&lt;/p&gt;

&lt;p&gt;A challenge remains communication with “legacy users” on older devices.
While most phones would handle the fallback SMS correctly, older feature phones
(“dumbphones”) in particular might display the verification message as multiple
confusing parts or silently drop it due to full inboxes, undermining the system.
To build trust for this fallback, a logical conclusion would be for participating
companies to form a “Spam Alliance”, complete with a website listing members, and
brand the message with this neutral alliance name.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;2026 footnote:&lt;/strong&gt; The DYST challenge-response pattern — “did you send this?” queried back to the originator’s domain — is the same model underpinning &lt;a href=&quot;https://live-verify.github.io/live-verify/&quot;&gt;Live Verify&lt;/a&gt; (&lt;a href=&quot;https://github.com/live-verify/live-verify&quot;&gt;source&lt;/a&gt;), but for documents instead of calls. A bank statement, police ID card, or sanctions attestation is normalized, SHA-256 hashed, and looked up at the issuer’s domain: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /v/{hash}&lt;/code&gt; — effectively asking the issuer “did you issue this?” The domain-as-authority principle is identical: the originating server is the source of truth, no central registry needed. Where DYST proposes a Spam Alliance coordinating telcos, Live Verify proposes &lt;a href=&quot;https://live-verify.github.io/live-verify/docs/authority-chain-spec&quot;&gt;authority chains&lt;/a&gt; — issuer endorsed by regulator endorsed by sovereign root — to establish trust without a single coordinating body.&lt;/p&gt;

&lt;p&gt;The telcos’ resistance would likely be because their large customers, such as
call centers using VoIP, would face significant hurdles. Much of their software
may be old and difficult to update to support a new verification protocol.
A call center vendor might even configure their system to automatically answer
“yes” to all challenges, legitimate or not. This would necessitate another layer
of defense, led by the Spam Alliance, to apply reputation scoring to the responders
themselves. If an endpoint blindly says “yes” to everything, its attestations
would eventually be deemed untrustworthy.&lt;/p&gt;

&lt;p&gt;Finally, it is instructive to look at the history of DMARC, SPF, and DKIM for
email. These technologies did not eliminate spam, but they largely solved the
problem of direct domain spoofing. Similarly, a DYST-like system for voice and SMS
would not be a silver bullet for all unwanted communication, but it could effectively
end caller-ID spoofing of legitimate numbers, forcing bad actors onto more easily
traceable and blockable channels.&lt;/p&gt;

&lt;h2 id=&quot;example-dyst-messages&quot;&gt;Example DYST Messages&lt;/h2&gt;

&lt;p&gt;As RCS (Rich Communication Services) supports rich cards and suggested actions,
the user experience for a DYST-style verification could be significantly improved
over a plain SMS fallback.&lt;/p&gt;

&lt;h3 id=&quot;challenge-json-payload-to-claimed-originator-who-has-a-dyst-enabled-smartphone&quot;&gt;Challenge (JSON payload) to claimed originator who has a DYST-enabled Smartphone&lt;/h3&gt;
&lt;p&gt;When the claimed originator’s Smartphone &lt;em&gt;is&lt;/em&gt; DYST-enabled, the verification happens
silently in the background between the originating service’s server and the handset.
This interaction uses a JSON payload, and the handset automatically answers the
challenge without user intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recieving handset sends this JSON payload to the DYST-enabled handset of the claimed originator:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;dyst_challenge&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;protocol_version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;1.0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;challenge_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;dyst-challenge-12345&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2025-11-29T10:30:00Z&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;originator_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;+14155550110&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;recipient_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;+12025550148&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;communication_type&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;voice_call&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;communication_id&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;call-abc-789&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;message_digest&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;sha256:abcdef12345...&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;ttl_seconds&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Claimed originators DYST-enabled handset responds silently:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;dyst_response&quot;: {
    &quot;challenge_id&quot;: &quot;dyst-challenge-12345&quot;,
    &quot;status&quot;: &quot;confirmed&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Or if they had not placed the voice call (someone else faked caller ID):&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;dyst_response&quot;: {
    &quot;challenge_id&quot;: &quot;dyst-challenge-12345&quot;,
    &quot;status&quot;: &quot;denied&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;challenge-rcs-message-to-claimed-originator-who-has-a-smartphone-thats-not-dyst-enabled-a-fallback&quot;&gt;Challenge RCS message to claimed originator who has a Smartphone that’s not DYST enabled (a fallback)&lt;/h3&gt;

&lt;p&gt;This is what the person &lt;em&gt;making&lt;/em&gt; the call would receive on their device if the
recipient doesn’t recognize them. It’s designed to be simple and quick.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Spam Alliance Verification ✓
---------------------------------------
🛡️ Did you just try to contact `+1-202-555-0148`?

To connect your call, please confirm it was you.

[ ✅ Yes, that was me ]  [ ❌ No, not me ]
---------------------------------------
&amp;lt;small&amp;gt;Sent by the Spam Alliance. [Learn More]&amp;lt;/small&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;challenge-sms-message-to-claimed-originator-with-a-non-rcs-phone&quot;&gt;Challenge SMS message to claimed originator with a non-RCS phone&lt;/h3&gt;
&lt;p&gt;If the originator’s phone does not support RCS (e.g., a “dumbphone”), the system
must fall back to plain SMS. This experience is the most basic and relies on the
user to manually reply.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Spam Alliance: Did you just try to contact +1-202-555-0148? To connect, reply YES to this message.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;language-selection-for-a-global-dyst-system&quot;&gt;Language Selection for a Global DYST System&lt;/h2&gt;

&lt;p&gt;For a global anti-spam system to be effective, it must communicate with users
in their native language. The DYST system would handle this differently depending
on the originator’s device capabilities.&lt;/p&gt;

&lt;h3 id=&quot;1-first-class-dyst-enabled-handset&quot;&gt;1. First-Class (DYST-enabled Handset)&lt;/h3&gt;
&lt;p&gt;This is the simplest scenario. The silent JSON payload exchanged between the
services is machine-readable and language-agnostic. If the originator’s handset
needs to display any notification related to the verification, it uses its own
local OS language setting to do so. No language information needs to be
transmitted in the challenge itself.&lt;/p&gt;

&lt;h3 id=&quot;2-rcs-fallback-non-dyst-smartphone&quot;&gt;2. RCS Fallback (non-DYST Smartphone)&lt;/h3&gt;
&lt;p&gt;When falling back to a user-facing RCS message, the system must send the
challenge in the correct language. This would be solved by having the originator’s
device report its language preference (e.g., ‘es-MX’ for Mexican Spanish) as part
of its standard RCS capabilities. The Spam Alliance’s service would then deliver
a pre-translated, interactive message in that specific language.&lt;/p&gt;

&lt;h3 id=&quot;3-sms-fallback-non-rcs-phone&quot;&gt;3. SMS Fallback (non-RCS phone)&lt;/h3&gt;
&lt;p&gt;This is the most challenging scenario. Like the RCS fallback, the system would
attempt to determine the device’s language. However, without the rich capabilities
of an RCS client, this may not be possible. As a last resort, the system would
have to make an educated guess based on the phone number’s country code, which is
less reliable but better than defaulting to a single language like English.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Modern CV Technology: JSON Resume embedded in HTML</title>
   <link href="https://paulhammant.com/2025/10/12/modern-cv-tech-json-resume-schema/"/>
   <updated>2025-10-12T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/10/12/modern-cv-tech-json-resume-schema</id>
   <content type="html">&lt;p&gt;Problem: uploading your resume/CV to a job portal should yield a perfectly parsed CV but often does not. This is true even if your template .docx is a claimed good starting point for later ingesting into such systems.&lt;/p&gt;

&lt;h1 id=&quot;building-the-future-of-digital-resumes-a-technical-deep-dive&quot;&gt;Building the Future of Digital Resumes: A Technical Deep Dive&lt;/h1&gt;

&lt;p&gt;In an era where Applicant Tracking Systems (ATS) and job portals increasingly dominate the recruitment landscape, the traditional PDF resume is showing its age. What if we could create a resume format that’s simultaneously machine-readable, human-friendly, and completely self-contained? Well, that ended up being a side project - read on.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-with-traditional-resumes&quot;&gt;The Problem with Traditional Resumes&lt;/h2&gt;

&lt;p&gt;Traditional resume formats present a fundamental challenge:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;PDFs&lt;/strong&gt; look great but are difficult for ATS systems to parse accurately, even in the nascent AI era.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Word documents&lt;/strong&gt; are editable but inconsistent across platforms&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plain text&lt;/strong&gt; is machine-readable but lacks visual appeal&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Linked In&lt;/strong&gt; would like to own this space, but they’re way too much lock in, and spend too much trying to keep your engagement in their pages, vs get you a job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;the-solution-json-resume-schema--interactive-html&quot;&gt;The Solution: JSON Resume Schema + Interactive HTML&lt;/h2&gt;

&lt;p&gt;This repository showcases a evolutionary approach that combines the best of two words - mnachine parsable and appealing to human eyes.  The raw storage of the CV/resume data:&lt;/p&gt;

&lt;p&gt;Every resume uses the official &lt;a href=&quot;https://jsonresume.org/&quot;&gt;JSON Resume&lt;/a&gt; standard:&lt;/p&gt;
&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;$schema&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;basics&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;contact&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;info and summary elemnts&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;work&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;employment&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;history blah blah blah&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;education&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;degrees&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;and certifications&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;skills&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;technical&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;abilities yada yada&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;and&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;ten more standardized sections&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each resume/CV is a single HTML file containing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Embedded JSON data above&lt;/strong&gt; in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag for ATS extraction&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inlined CSS&lt;/strong&gt; (~1000 lines) for complete visual control&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inlined JavaScript&lt;/strong&gt; (~1200 lines) for interactive features&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Zero external dependencies&lt;/strong&gt; for viewing (optional CDN for PDF generation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same file serves two masters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Machines&lt;/strong&gt;: Extract structured JSON data for database import. It could have as easily been XML or YAML, but JSON parsing in web pages is built in.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Humans&lt;/strong&gt;: Styled and responsive HTML with some interactive features&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;some-interactivity-to-control-verbosity&quot;&gt;Some interactivity to control verbosity&lt;/h2&gt;

&lt;p&gt;You can click a pen to go into edit mode. Editing isn’t text, it is contract (-) and expand (+) affordances to 
reduce the verbosity of sections. While I may gush about the origin story of Selenium an in-firm recruiter, then agent who’s taken my CV to them, the downstream interviewers are spectacularly uninterested in that so they’ll hit (-) to collapse that section. This will persist if you go on to print the resume/CV, but not if you close the tab.&lt;/p&gt;

&lt;p&gt;Collapse affordance shown:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/princess_leia1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;That clicked, expand affordance shown:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://paulhammant.com/images/princess_leia2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is only useful for someone wanting customize the document for a purpose. For example, and interview stage with the candidate or a discussion with a colleague about take forward in the process or decline.&lt;/p&gt;

&lt;h3 id=&quot;responsive-design-with-print-optimization&quot;&gt;Responsive Design with Print Optimization&lt;/h3&gt;

&lt;p&gt;The CSS includes specialized media queries to ensures URLs are visible in printed versions—crucial for ATS systems and recruiters.  Versus just hyperlinks when that’s in a browser. No big deal perhaps.&lt;/p&gt;

&lt;h3 id=&quot;pdf-generation-pipeline&quot;&gt;PDF Generation Pipeline&lt;/h3&gt;

&lt;p&gt;For enhanced PDF output, the system dynamically loads:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;html2canvas&lt;/strong&gt;: Renders HTML to canvas with pixel-perfect accuracy&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;jsPDF&lt;/strong&gt;: Converts canvas to professional PDF format&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ignoring this lazy load of JavaScript from CDNs, this was otherwise a zero dependency tech.&lt;/p&gt;

&lt;h2 id=&quot;sample-cvs-in-the-repository&quot;&gt;Sample CVs in the repository&lt;/h2&gt;

&lt;p&gt;Current collection includes &lt;strong&gt;14 example resumes&lt;/strong&gt; featuring fictional characters:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Technical roles&lt;/strong&gt;: Tony Stark (Genius/Inventor), Harold Finch (Software Engineer)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Leadership positions&lt;/strong&gt;: T’Challa (Head of State), Princess Leia (Rebel Leader)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Diverse backgrounds&lt;/strong&gt;: Hermione Granger (Academic), Mulan (Military Officer)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Take a look at &lt;strong&gt;Sam “Root” Groves&lt;/strong&gt; from “Person of Interest” TV series; &lt;a href=&quot;https://paul-hammant.github.io/better-cv-tech/Samantha_Groves_Resume.html&quot;&gt;paul-hammant.github.io/better-cv-tech/Samantha_Groves_Resume.html&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;implementation-highlights&quot;&gt;Implementation Highlights&lt;/h2&gt;

&lt;h3 id=&quot;markdown-support-within-json&quot;&gt;Markdown Support Within JSON&lt;/h3&gt;

&lt;p&gt;The system supports basic markdown in key text fields:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**bold text**&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*italic text*&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[link text](https://example.com)&lt;/code&gt; for clickable links&lt;/li&gt;
  &lt;li&gt;Paragraph breaks with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\n\n&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This enhances human readability while maintaining ATS compatibility. Well, maybe.&lt;/p&gt;

&lt;h2 id=&quot;ats-integration-strategy&quot;&gt;ATS Integration Strategy&lt;/h2&gt;

&lt;p&gt;For ATS systems to adopt this format, they need minimal changes:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Extract resume data from HTML&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resumeScript&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;cv-data-json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;resumeData&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;resumeScript&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;textContent&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Now import structured data directly into database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Likely they’d just be snipping out of the unparsed source file though. That would go into their databases, but also maybe a pipeline that uses a JSON Resume to HTML pipeline. Either way, this is &lt;strong&gt;orders of magnitude&lt;/strong&gt; more reliable than PDF or .docx text extraction or HTML scraping. Even with claimed AI on their side in 2025&lt;/p&gt;

&lt;h2 id=&quot;performance-characteristics&quot;&gt;Performance Characteristics&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;File size&lt;/strong&gt;: ~80KB per resume (including all assets)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Load time&lt;/strong&gt;: Near instant (no external requests for viewing)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Browser support&lt;/strong&gt;: Modern browsers with JavaScript turned on (ES6+ required)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mobile responsive&lt;/strong&gt;: Breakpoints at 768px and 480px&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;security-considerations&quot;&gt;Security Considerations&lt;/h2&gt;

&lt;p&gt;For recruiters receiving HTML resumes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;✅ &lt;strong&gt;No file system access&lt;/strong&gt; - Pure DOM manipulation&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;No external requests&lt;/strong&gt; - Self-contained execution&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;Standard JavaScript&lt;/strong&gt; - No eval() or dangerous APIs&lt;/li&gt;
  &lt;li&gt;✅ &lt;strong&gt;Data transparency&lt;/strong&gt; - JSON visible in source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Likely there will be some over-cautiousness. Bigger companies could verify the JavaScript within each uploaded CV/resume if they really wanted to. A whitelist of sorts (extract &amp;gt; lint &amp;gt; pretty-print &amp;gt; SHA256 &amp;gt; check against whitelist). Or just&lt;/p&gt;

&lt;h2 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h2&gt;

&lt;p&gt;To create your own resume:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Copy a template&lt;/strong&gt;: Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Lorem_Ipsum_Resume.html&lt;/code&gt; as your starting point&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Replace JSON data&lt;/strong&gt;: Update the embedded resume data with your information&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Inline css and javascript&lt;/strong&gt;: They are separate in the repo, as there is fourteen or so sample CV/resumes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Test thoroughly&lt;/strong&gt;: Verify print output and mobile responsiveness&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Name appropriately&lt;/strong&gt;: Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FirstName_LastName_Resume.html&lt;/code&gt; format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or get AI to take the constituent pieces and make the page for you. It did so for mine in a 
couple of mins. Prompt is in the repo.&lt;/p&gt;

&lt;h2 id=&quot;links&quot;&gt;Links:&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repository&lt;/strong&gt;: &lt;a href=&quot;https://github.com/paul-hammant/better-cv-tech&quot;&gt;paul-hammant/better-cv-tech&lt;/a&gt;&lt;br /&gt;
&lt;strong&gt;Live Demo&lt;/strong&gt;: &lt;a href=&quot;https://paul-hammant.github.io/better-cv-tech/&quot;&gt;GitHub Pages Gallery - 14 resume/CVs&lt;/a&gt;&lt;br /&gt;
&lt;strong&gt;Schema&lt;/strong&gt;: &lt;a href=&quot;https://jsonresume.org/schema/&quot;&gt;JSON Resume v1.0.0&lt;/a&gt;&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Building a Secure Container Sandbox on ChromeOS for Testing Untrusted Code</title>
   <link href="https://paulhammant.com/2025/09/18/workstation-sandbox-blues/"/>
   <updated>2025-09-18T00:00:00+00:00</updated>
   <id>https://paulhammant.com/2025/09/18/workstation-sandbox-blues</id>
   <content type="html">&lt;h2 id=&quot;the-problem-running-random-github-code-safely&quot;&gt;The Problem: Running Random GitHub Code Safely&lt;/h2&gt;

&lt;p&gt;As developers, we frequently encounter interesting GitHub repositories, development tools, or scripts that we want to test. However, running` untrusted code directly on our development machines poses significant security risks:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Supply chain attacks&lt;/strong&gt;: Malicious code that modifies system binaries or installs backdoors (like the 2025 Chalk npm package compromise that affected millions of downloads)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Data theft&lt;/strong&gt;: Scripts that scan for SSH keys (passphrase protected or not), API tokens, or sensitive files&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;System compromise&lt;/strong&gt;: Privilege escalation attacks that gain persistent access&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Resource abuse&lt;/strong&gt;: Cryptocurrency miners or botnet participation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional solutions like virtual machines are heavyweight and slow to reset. Docker provides isolation but shares the kernel and can be escaped. ChromeOS’s unique architecture offers a compelling alternative through its layered container system.&lt;/p&gt;

&lt;h2 id=&quot;chromeos-container-architecture-defense-in-depth&quot;&gt;ChromeOS Container Architecture: Defense in Depth&lt;/h2&gt;

&lt;p&gt;ChromeOS provides multiple layers of isolation that make it ideal for secure sandboxing:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────────────────────────────────────────-─────┐
│              ChromeOS (Host)                 │
│  ┌─────────────────────────────────────────┐ │
│  │           Termina VM                    │ │
│  │  ┌─────────────┐  ┌─────────────────────┤ │
│  │  │   Penguin   │  │        OSS          │ │
│  │  │  (Trusted)  │  │   (Untrusted)       │ │
│  │  │             │  │                     │ │
│  │  │ - Your work │  │ - Random GitHub     │ │
│  │  │ - SSH keys  │  │   repositories      │ │
│  │  │ - Configs   │  │ - Untested tools    │ │
│  │  │             │  │ - No sensitive      │ │
│  │  │             │  │   data              │ │
│  │  └─────────────┘  └─────────────────────┤ │
│  └─────────────────────────────────────────┘ │
└─────────────────────────────────────────-────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or a second ascii-art way of outlining the same situation, with more detail ()&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChromeOS (host)
 └─ crosvm (VM with KVM acceleration if hardware supports it)
     └─ Termina (tiny VM OS, runs LXD daemon and socket)
         │    └- uses LXD API to query kernel level cgroups/namespaces
         │    └- avoids trusting oss&apos;s /bin/ps /bin/find etc
         │         
         ├─ oss       (Debian container, untrusted)
         │    └─ [supply-chain attacker could replace ps/find/ls]
         │
         └─ penguin   (main Debian container, trusted code)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;complete-setup-process&quot;&gt;Complete Setup Process&lt;/h2&gt;

&lt;h3 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h3&gt;

&lt;p&gt;This setup must be run in ChromeOS’s Termina VM, not inside a container. You can access this by pressing ctrl-alt-t and the resulting terminal should say “Welcome to crosh, the ChromeOS developer shell.” You should see a prompt like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;crosh&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3 id=&quot;quick-start-script&quot;&gt;Quick Start Script&lt;/h3&gt;

&lt;p&gt;There’s no vi, emacs or nano in chrosh. You’ll prepare scripts elsewhere and email them to yourself. You’ll use ChromeOS’ regular mail app to read those, as you logged in with your google account after all. The ChromeOS text editor is a bit weak for my liking, and I like to keep a copy of things that may get refined and are not canonically in source control.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/filename_as_instructed.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SETUP_SCRIPT_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos; | bash
The bash code below 
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SETUP_SCRIPT_EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Save this as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/tmp/setup-secure-containers.sh&lt;/code&gt; and run it with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash /tmp/setup-secure-containers.sh&lt;/code&gt; as you can’t make it executable:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#!/bin/bash&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# ChromeOS Secure Container Setup&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# This creates two containers:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - penguin (default/trusted) - your main work environment  &lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - oss (untrusted) - for running cloned repos and untrusted code&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# IMPORTANT: This script must be run in Termina (the ChromeOS VM)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# because lxc commands don&apos;t work from inside containers&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;set&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Colors for output&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0;31m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;GREEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0;32m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[1;33m&apos;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;\033[0m&apos;&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;# No Color&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=================================================&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ChromeOS Secure Container Setup&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=================================================&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;This script will create a two-container security setup:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- penguin: Your trusted container (already exists)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- oss: Untrusted container for running random code&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Verify we&apos;re in Termina&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; lxc &amp;amp;&amp;gt; /dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ERROR: lxc command not found. This script must be run in Termina&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Open the Terminal app in ChromeOS and run this script there.&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Environment check passed - lxc command found&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 1: Show current containers&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 1: Current container status&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;================================&quot;&lt;/span&gt;
lxc list

&lt;span class=&quot;c&quot;&gt;# Step 2: Get available images&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 2: Finding Debian image...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;===============================&quot;&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;IMAGE_FINGERPRINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$IMAGE_FINGERPRINT&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;No Debian image found. Available images:&quot;&lt;/span&gt;
    lxc image list
    &lt;span class=&quot;nb&quot;&gt;exit &lt;/span&gt;1
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 3: Create the oss container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 3: Creating oss container...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Check if oss container already exists&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if &lt;/span&gt;lxc info oss &amp;amp;&amp;gt;/dev/null&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Container &apos;oss&apos; already exists, skipping...&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;else
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Creating &apos;oss&apos; container...&quot;&lt;/span&gt;
    lxc launch &lt;span class=&quot;nv&quot;&gt;$IMAGE_FINGERPRINT&lt;/span&gt; oss
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Waiting for container to start...&quot;&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;sleep &lt;/span&gt;5
&lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Show updated container list&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
lxc list

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 4: Setting resource limits for oss container...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;====================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Set resource limits and security restrictions&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.nesting &lt;span class=&quot;nb&quot;&gt;false
&lt;/span&gt;lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.privileged &lt;span class=&quot;nb&quot;&gt;false

echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Resource limits and security restrictions applied to oss container&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 5: Setting up oss container for untrusted code...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;=====================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Install packages in oss container&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; git build-essential python3 python3-pip curl wget nodejs npm
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /workspace
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;Untrusted code workspace - Only run random GitHub repos here!&apos; &amp;gt; /workspace/README.txt&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;OSS container setup complete&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 6: Set up baseline in trusted container (penguin)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 6: Creating baseline in penguin (trusted container)...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==========================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create baseline hashes for binary integrity monitoring&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;find /bin /usr/bin -type f -executable -exec sha256sum {} &lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; &amp;gt; /root/baseline_hashes.txt&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;Baseline created at &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&apos; &amp;gt;&amp;gt; /root/baseline_hashes.txt&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;wc -l &amp;lt; /root/baseline_hashes.txt | xargs -I {} echo &apos;Baseline hash count: {}&apos;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Ensure penguin has essential tools&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; vim git

&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Baseline created in penguin container&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 7: Create monitoring script in Termina&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 7: Creating Termina-based monitoring script...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;================================================&quot;&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/monitor-containers.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MONITOR_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
# Container Security Monitor - Runs in Termina
# Monitors the oss (untrusted) and penguin (trusted) containers for supply chain attacks

RED=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0;31m&apos;
GREEN=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0;32m&apos;
YELLOW=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[1;33m&apos;
NC=&apos;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\0&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;33[0m&apos;

function setup_baseline() {
    local baseline_file=&quot;/tmp/.container_baseline&quot;
    
    if [ ! -f &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
        echo &quot;Creating baseline for binary integrity monitoring...&quot;
        echo &quot;# Container Security Baseline - Created &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        
        # Create baseline for both containers
        for container in oss penguin; do
            if lxc info &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;amp;&amp;gt;/dev/null; then
                echo &quot;# &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; container baseline&quot; &amp;gt;&amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                for binary in /bin/bash /bin/sh /usr/bin/python3 /usr/bin/node /usr/bin/git; do
                    hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
                    if [ -n &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                        echo &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;gt;&amp;gt; &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                    fi
                done
            fi
        done
        echo &quot;Baseline created at &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$baseline_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
}

function check_binary_integrity() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    local found_issues=0
   
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Binary Integrity Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Check key binaries against baseline
    while IFS=: read -r base_container base_binary base_hash; do
        if [[ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; == &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]] &amp;amp;&amp;amp; [[ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; =~ ^/.*&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;]]; then
            current_hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
            if [ -n &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ] &amp;amp;&amp;amp; [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; != &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  Modified: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_binary&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                echo &quot;    Expected: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$base_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                echo &quot;    Current:  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$current_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
                found_issues=1
            fi
        fi
    done &amp;lt; &quot;/tmp/.container_baseline&quot; 2&amp;gt;/dev/null
    
    if [ &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$found_issues&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -eq 0 ]; then
        echo &quot;  All monitored binaries match baseline&quot;
    fi
    
    return &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$found_issues&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
}

function check_processes() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Process Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Top processes:&quot;
    
    # Show top CPU-consuming processes
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- ps aux --sort=-%cpu 2&amp;gt;/dev/null | head -6 | while IFS= read -r line; do
        echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
}

function check_network() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Network Connections - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
   
    # Count listening ports and established connections
    listening=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tlnp&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;LISTEN | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    connections=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tnp&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;ESTAB | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    echo &quot;  Listening ports: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$listening&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Active connections: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$connections&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Alert on any external connections from untrusted container
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; = &quot;oss&quot; ] &amp;amp;&amp;amp; [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$connections&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; -gt 0 ]; then
        echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  External connections detected in untrusted container:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- ss -tnp 2&amp;gt;/dev/null | grep ESTAB | while IFS= read -r line; do
            echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        done
    fi
}

function check_recent_files() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Recently Modified Files - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;  Files modified in last 10 minutes:&quot;
   
    # Focus on system directories for supply chain attacks
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find /bin /usr/bin /lib -xdev -type f -mmin -10 2&amp;gt;/dev/null | while IFS= read -r line; do
        echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
    
    # Also check workspace for oss
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; = &quot;oss&quot; ]; then
        echo &quot;  Workspace files:&quot;
        lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find /workspace -xdev -type f -mmin -10 2&amp;gt;/dev/null | head -5 | while IFS= read -r line; do
            echo &quot;    &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$line&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        done
    fi
}

function check_suspicious_files() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;YELLOW&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;Suspicious Files Check - &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    
    # Look for hidden files in tmp directories
    suspicious_count=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp /var/tmp /dev/shm &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    if [ &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suspicious_count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; -gt 0 ]; then
        echo -e &quot;  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  Found &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suspicious_count&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; hidden files in temp directories&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    else
        echo &quot;  No suspicious hidden files found&quot;
    fi
}

function check_suid_changes() {
    local container=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    
    # Check for new SUID binaries
    lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find / -xdev -perm -4000 -type f 2&amp;gt;/dev/null | while read suid_file; do
        echo &quot;    SUID: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$suid_file&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    done
}

function setup_suid_baseline() {
    for container in oss penguin; do
        if lxc info &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; &amp;amp;&amp;gt;/dev/null; then
            if [ ! -f &quot;/tmp/.known_suid_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot; ]; then
                lxc exec &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; -- find / -xdev -perm -4000 -type f 2&amp;gt;/dev/null &amp;gt; &quot;/tmp/.known_suid_&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
            fi
        fi
    done
}

# Main monitoring loop
echo &quot;=================================================&quot;
echo &quot;Container Security Monitor&quot;
echo &quot;=================================================&quot;
echo &quot;Monitoring for supply chain attacks in:&quot;
echo &quot;  - oss (untrusted) - where you run random code&quot;
echo &quot;  - penguin (trusted) - your main environment&quot;
echo &quot;&quot;

# Setup baselines on first run
setup_baseline
setup_suid_baseline

while true; do
    clear
    echo &quot;Container Security Monitor - &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    echo &quot;=====================================&quot;
    echo &quot;&quot;
   
    # Monitor the untrusted container more closely
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=== OSS Container (UNTRUSTED) ===&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    if ! check_binary_integrity &quot;oss&quot;; then
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  ALERT: Binary modification detected in OSS container!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;This could indicate a supply chain attack from recently run code&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
    echo &quot;&quot;
    check_processes &quot;oss&quot;
    echo &quot;&quot;
    check_network &quot;oss&quot;
    echo &quot;&quot;
    check_recent_files &quot;oss&quot;
    echo &quot;&quot;
    check_suspicious_files &quot;oss&quot;
   
    echo &quot;&quot;
    echo -e &quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GREEN&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;=== Penguin Container (TRUSTED) ===&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    if ! check_binary_integrity &quot;penguin&quot;; then
        echo -e &quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;!!!  CRITICAL: Binary modification in TRUSTED container!&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
    fi
    echo &quot;&quot;
    check_network &quot;penguin&quot;
   
    echo &quot;&quot;
    echo &quot;Next scan in 30 seconds... (Press Ctrl+C to exit)&quot;
    sleep 30
done
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MONITOR_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/monitor-containers.sh
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Monitoring script created at /tmp/monitor-containers.sh&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Step 8: Create helper scripts&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 8: Creating helper scripts...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create a script to enter each container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/enter-oss.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_OSS_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;=========================================&quot;
echo &quot;Entering OSS (UNTRUSTED) Container&quot;
echo &quot;=========================================&quot;
echo &quot;!!!  SECURITY WARNING:&quot;
echo &quot;- Only run untrusted code here&quot;
echo &quot;- No sensitive files or keys&quot;
echo &quot;- Monitor with: bash /tmp/monitor-containers.sh&quot;
echo &quot;=========================================&quot;
lxc exec oss -- bash
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_OSS_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/enter-oss.sh

&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/enter-penguin.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_PENGUIN_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;=========================================&quot;
echo &quot;Entering Penguin (TRUSTED) Container&quot;
echo &quot;=========================================&quot;
echo &quot;This is your trusted development environment&quot;
echo &quot;=========================================&quot;
lxc exec penguin -- bash
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;ENTER_PENGUIN_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/enter-penguin.sh

&lt;span class=&quot;c&quot;&gt;# Create status script&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/status.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STATUS_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;Container Security Setup Status&quot;
echo &quot;===============================&quot;
lxc list
echo &quot;&quot;
echo &quot;OSS Container Security Settings:&quot;
echo &quot;  CPU limit: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss limits.cpu&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Memory limit: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss limits.memory&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Privileged: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss security.privileged&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;  Nesting: &lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc config get oss security.nesting&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
echo &quot;&quot;
echo &quot;Quick security check:&quot;
for container in oss penguin; do
    echo -n &quot;  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; binary integrity: &quot;
    ps_hash=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; /bin/ps 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print substr($1,1,8)}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    echo &quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ps_hash&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&quot;
done
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;STATUS_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/status.sh

&lt;span class=&quot;c&quot;&gt;# Create a reset script for the oss container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/reset-oss.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RESET_EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
echo &quot;This will destroy and recreate the OSS container&quot;
echo &quot;Any data in the OSS container will be lost!&quot;
read -p &quot;Are you sure? (y/N): &quot; -n 1 -r
echo
if [[ &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$REPLY&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; =~ ^[Yy]&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;]]; then
    echo &quot;Resetting OSS container...&quot;
    lxc stop oss
    lxc delete oss
    
    # Recreate with same settings
    IMAGE_FP=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;
    lxc launch &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$IMAGE_FP&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt; oss
    
    # Reapply security settings
    lxc config set oss limits.cpu 2
    lxc config set oss limits.memory 2GB
    lxc config set oss security.nesting false
    lxc config set oss security.privileged false
    
    # Reinstall packages
    lxc exec oss -- apt-get update
    lxc exec oss -- apt-get install -y git build-essential python3 python3-pip curl wget nodejs npm
    lxc exec oss -- mkdir -p /workspace
    
    echo &quot;OSS container reset complete&quot;
    
    # Update baseline
    rm -f /tmp/.container_baseline
    echo &quot;Run: bash /tmp/monitor-containers.sh to recreate baseline&quot;
fi
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;RESET_EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/reset-oss.sh

&lt;span class=&quot;c&quot;&gt;# Step 9: Optional SSH Setup for OSS Container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Step 9: Setting up SSH access to OSS (optional)...&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;==================================================&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Install and configure SSH&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; openssh-server
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/ssh/sshd_not_to_be_run
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ssh-keygen &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; systemctl restart ssh
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; systemctl &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;ssh

&lt;span class=&quot;c&quot;&gt;# Create non-root user&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; useradd &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; /bin/bash dev 2&amp;gt;/dev/null &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;true
&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;echo &apos;dev:changeme&apos; | chpasswd&quot;&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; usermod &lt;span class=&quot;nt&quot;&gt;-aG&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;dev

&lt;span class=&quot;c&quot;&gt;# Get IP address&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;OSS_IP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc list oss &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; csv &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; 4 | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos; &apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SSH access configured:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  ssh dev@&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$OSS_IP&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  Default password: changeme (change immediately!)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;!!!  WARNING: Never use SSH agent forwarding (-A) with untrusted containers!&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Final summary&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Setup Complete!&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;===============&quot;&lt;/span&gt;
lxc list
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Available commands:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/monitor-containers.sh - Start security monitoring&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/enter-oss.sh         - Enter untrusted container&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/enter-penguin.sh     - Enter trusted container&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/status.sh            - Check security status&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;  bash /tmp/reset-oss.sh         - Reset OSS container (if compromised)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Usage pattern:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1. Run random code ONLY in oss container (bash /tmp/enter-oss.sh)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2. Keep monitoring running in another terminal (bash /tmp/monitor-containers.sh)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3. If monitor detects modified binaries, consider using reset script&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;The monitor will detect:&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Modified system binaries (supply chain attacks)&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Suspicious processes and network connections&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Recently modified files in system directories&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;- Hidden files in temporary directories&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;🔒 Your trusted work remains safe in the penguin container!&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;manual-setup-steps&quot;&gt;Manual Setup Steps&lt;/h3&gt;

&lt;p&gt;If you prefer to understand each step, here’s the manual process:&lt;/p&gt;

&lt;h4 id=&quot;1-create-the-untrusted-container&quot;&gt;1. Create the Untrusted Container&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Get available image&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;IMAGE_FP&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc image list &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt; csv | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; debian | &lt;span class=&quot;nb&quot;&gt;cut&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;,&apos;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f2&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Create OSS container&lt;/span&gt;
lxc launch &lt;span class=&quot;nv&quot;&gt;$IMAGE_FP&lt;/span&gt; oss

&lt;span class=&quot;c&quot;&gt;# Set security restrictions&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.nesting &lt;span class=&quot;nb&quot;&gt;false
&lt;/span&gt;lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss security.privileged &lt;span class=&quot;nb&quot;&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;2-install-development-tools&quot;&gt;2. Install Development Tools&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Update and install common development tools&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get update
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; git build-essential python3 python3-pip curl wget nodejs npm

&lt;span class=&quot;c&quot;&gt;# Create workspace directory&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /workspace
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;3-create-monitoring-script&quot;&gt;3. Create Monitoring Script&lt;/h4&gt;

&lt;p&gt;The monitoring script runs from Termina and watches both containers for signs of compromise:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create the monitoring script&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/monitor-containers.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
#!/bin/bash
# [Full monitoring script from above]
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF

&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;chmod&lt;/span&gt; +x /tmp/monitor-containers.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;troubleshooting-common-issues&quot;&gt;Troubleshooting Common Issues&lt;/h2&gt;

&lt;h3 id=&quot;termina-filesystem-issues&quot;&gt;Termina Filesystem Issues&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: Scripts fail with “Read-only file system” error. Termina’s home directory (~/) is read-only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Use /tmp for all scripts which is not read only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue&lt;/strong&gt;: no editors&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt; pipe to file trick as mentioned.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Correct - use /tmp&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /tmp/script.sh &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;sh&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF&lt;/span&gt;&lt;span class=&quot;sh&quot;&gt;&apos;
...
&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;EOF
&lt;/span&gt;bash /tmp/script.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;ssh-setup&quot;&gt;SSH Setup&lt;/h3&gt;

&lt;p&gt;SSH service needs manual setup in containers:&lt;/p&gt;

&lt;p&gt;You get into the container using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lxc exec container_name -- bash&lt;/code&gt; from Termina (crosh):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# In the target container (run via lxc exec)&lt;/span&gt;
apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; openssh-server
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /etc/ssh/sshd_not_to_be_run  &lt;span class=&quot;c&quot;&gt;# Remove startup blocker&lt;/span&gt;
ssh-keygen &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt;                       &lt;span class=&quot;c&quot;&gt;# Generate host keys&lt;/span&gt;
systemctl restart ssh
systemctl &lt;span class=&quot;nb&quot;&gt;enable &lt;/span&gt;ssh
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;root:changeme&apos;&lt;/span&gt; | chpasswd    &lt;span class=&quot;c&quot;&gt;# Set password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;lxc-command-not-found&quot;&gt;LXC Command Not Found&lt;/h3&gt;

&lt;p&gt;Do NOT run from inside penguin or any other container - you need to be in crosh: ctrl-alt-t&lt;/p&gt;

&lt;h2 id=&quot;secure-git-access-patterns&quot;&gt;Secure Git Access Patterns&lt;/h2&gt;

&lt;p&gt;You don’t want you SSH private key on the container that may be taken over by ‘chalk’ style actions. At least you don’t want it without a lengthy passphrase, but there are traditional solutions:&lt;/p&gt;

&lt;h3 id=&quot;the-ssh-agent-forwarding-security-risk&quot;&gt;The SSH Agent Forwarding Security Risk&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical Warning&lt;/strong&gt;: SSH agent forwarding allows untrusted code to use your keys!&lt;/p&gt;

&lt;p&gt;While your SSH session with -A flag is active:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Malicious code can push to ANY repo you have write access to&lt;/li&gt;
  &lt;li&gt;Can clone ANY private repo you have access to&lt;/li&gt;
  &lt;li&gt;Cannot steal your key, but can USE it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;safer-alternatives-for-git-access&quot;&gt;Safer Alternatives for Git Access&lt;/h3&gt;

&lt;h4 id=&quot;option-1-read-only-deploy-keys-recommended&quot;&gt;Option 1: Read-Only Deploy Keys (RECOMMENDED)&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create separate key for untrusted work&lt;/span&gt;
ssh-keygen &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ed25519 &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; ~/.ssh/oss_readonly_key &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Add to GitHub as deploy key (READ-ONLY) for specific repos&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Copy ONLY this key to oss container&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;penguin &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /home/USER/.ssh/oss_readonly_key | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; bash &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;cat &amp;gt; /home/dev/.ssh/id_ed25519 &amp;amp;&amp;amp; chmod 600 /home/dev/.ssh/id_ed25519&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;option-2-fine-grained-personal-access-tokens&quot;&gt;Option 2: Fine-Grained Personal Access Tokens&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create token with minimal permissions (public_repo only)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Use in oss container:&lt;/span&gt;
git clone https://TOKEN@github.com/user/repo.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;option-3-time-limited-agent-forwarding-use-sparingly&quot;&gt;Option 3: Time-Limited Agent Forwarding (USE SPARINGLY)&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Only when absolutely necessary for push operations&lt;/span&gt;
ssh &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; dev@[oss-ip]
&lt;span class=&quot;c&quot;&gt;# Do your git operation&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# EXIT IMMEDIATELY&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or use confirmation-required keys&lt;/span&gt;
ssh-add &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; ~/.ssh/id_ed25519  &lt;span class=&quot;c&quot;&gt;# Requires confirmation for each use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;security-best-practices&quot;&gt;Security Best Practices&lt;/h2&gt;

&lt;h3 id=&quot;what-to-never-do&quot;&gt;What to NEVER Do&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Don’t store sensitive data in the OSS container&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; ~/.ssh/id_rsa /path/to/oss/container
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Don’t run trusted code in OSS container&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec &lt;/span&gt;oss &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; git clone git@github.com:yourcompany/private-repo.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Don’t disable monitoring&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# NEVER DO THIS - Always keep monitoring running&lt;/span&gt;
pkill monitor-containers
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;what-to-always-do&quot;&gt;What to ALWAYS Do&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Reset compromised containers&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# If monitor detects issues&lt;/span&gt;
bash /tmp/reset-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
    &lt;p&gt;Don’t attempt to repair them. Heck, maybe reset with some regularity anyway.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Regularly update baselines&lt;/strong&gt;:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# After legitimate updates&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;rm&lt;/span&gt; /tmp/.container_baseline
bash /tmp/monitor-containers.sh  &lt;span class=&quot;c&quot;&gt;# Recreates baseline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;container-isolation-rules&quot;&gt;Container Isolation Rules&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Container&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
      &lt;th&gt;SSH Keys&lt;/th&gt;
      &lt;th&gt;Git Configs&lt;/th&gt;
      &lt;th&gt;Sensitive Data&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;penguin&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Trusted development&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
      &lt;td&gt;Safe&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;strong&gt;oss&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;Untrusted testing&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;git-security-matrix&quot;&gt;Git Security Matrix&lt;/h3&gt;

&lt;table class=&quot;table table-striped&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Operation&lt;/th&gt;
      &lt;th&gt;Penguin (Trusted)&lt;/th&gt;
      &lt;th&gt;OSS (Untrusted)&lt;/th&gt;
      &lt;th&gt;Method&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Clone public repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;HTTPS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Clone private repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;!&lt;/td&gt;
      &lt;td&gt;Deploy keys only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Push to repos&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;N&lt;/td&gt;
      &lt;td&gt;Never from OSS&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Store SSH keys&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;N&lt;/td&gt;
      &lt;td&gt;Never&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Store PATs&lt;/td&gt;
      &lt;td&gt;y&lt;/td&gt;
      &lt;td&gt;!&lt;/td&gt;
      &lt;td&gt;Limited scope only&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Agent forwarding&lt;/td&gt;
      &lt;td&gt;N/A&lt;/td&gt;
      &lt;td&gt;!️&lt;/td&gt;
      &lt;td&gt;Brief sessions only&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;container-access-patterns&quot;&gt;Container Access Patterns&lt;/h3&gt;

&lt;h4 id=&quot;daily-workflow&quot;&gt;Daily workflow&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;Terminal App -&amp;gt; penguin        # Normal development&lt;/li&gt;
  &lt;li&gt;Ctrl+Alt+T -&amp;gt; vsh termina -&amp;gt; bash /tmp/enter-oss.sh  # Testing&lt;/li&gt;
  &lt;li&gt;Or ssh from penguin to oss&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;security-monitoring&quot;&gt;Security monitoring&lt;/h4&gt;

&lt;p&gt;Ctrl+Alt+T -&amp;gt; vsh termina -&amp;gt; bash /tmp/monitor-containers.sh&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Never create shortcuts that bypass security&lt;/li&gt;
  &lt;li&gt;Don’t alias direct access to OSS in penguin&lt;/li&gt;
  &lt;li&gt;Don’t auto-start monitoring (review alerts manually)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;lessons-from-production-use&quot;&gt;Lessons from Production Use&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key Insight&lt;/strong&gt;: The separation between Termina (VM host) and containers is crucial. Many security solutions try to work entirely within containers, but the real power comes from leveraging the host-level view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we learned&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Container isolation is only as good as your monitoring for breaches&lt;/li&gt;
  &lt;li&gt;Host-level monitoring provides better security visibility&lt;/li&gt;
  &lt;li&gt;ChromeOS’s architecture is designed for this type of security model&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;understanding-the-monitoring-system&quot;&gt;Understanding the Monitoring System&lt;/h2&gt;

&lt;p&gt;The monitoring system watches for several types of compromise:&lt;/p&gt;

&lt;h3 id=&quot;1-binary-integrity-monitoring&quot;&gt;1. Binary Integrity Monitoring&lt;/h3&gt;

&lt;p&gt;I grant you this is underdeveloped at the point of this blog entry.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Creates baseline hashes of critical binaries&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;binary &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /bin/bash /bin/sh /usr/bin/python3 /usr/bin/node /usr/bin/git&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
    &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sha256sum&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $1}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$binary&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$hash&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.container_baseline
&lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Supply chain attacks that modify system binaries&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-world example&lt;/strong&gt;: The 2024 Chalk npm package compromise replaced legitimate packages with malicious versions that could modify Node.js binaries or install backdoors. Our monitoring would detect such changes immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example alert&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;!!!  Modified: /usr/bin/python3
Expected: a1b2c3d4e5f6...
Current:  x9y8z7w6v5u4...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-process-monitoring&quot;&gt;2. Process Monitoring&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Shows CPU-intensive processes&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ps aux &lt;span class=&quot;nt&quot;&gt;--sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;-%cpu | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Cryptocurrency miners, botnet activity, unexpected daemons&lt;/p&gt;

&lt;h3 id=&quot;3-network-connection-analysis&quot;&gt;3. Network Connection Analysis&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Counts active connections&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;connections&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; ss &lt;span class=&quot;nt&quot;&gt;-tnp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;ESTAB | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Data exfiltration, command &amp;amp; control communication, unexpected servers&lt;/p&gt;

&lt;h3 id=&quot;4-file-system-changes&quot;&gt;4. File System Changes&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Finds recently modified system files&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /bin /usr/bin /lib &lt;span class=&quot;nt&quot;&gt;-xdev&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f &lt;span class=&quot;nt&quot;&gt;-mmin&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: System file tampering, backdoor installation&lt;/p&gt;

&lt;h3 id=&quot;5-hidden-file-detection&quot;&gt;5. Hidden File Detection&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Searches for hidden files in temp directories&lt;/span&gt;
lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp /var/tmp /dev/shm &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;.*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-type&lt;/span&gt; f
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What it detects&lt;/strong&gt;: Malware staging areas, credential harvesting tools&lt;/p&gt;

&lt;h2 id=&quot;advanced-usage-patterns&quot;&gt;Advanced Usage Patterns&lt;/h2&gt;

&lt;h3 id=&quot;testing-suspicious-github-repositories&quot;&gt;Testing Suspicious GitHub Repositories&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. Start monitoring (separate terminal)&lt;/span&gt;
bash /tmp/monitor-containers.sh

&lt;span class=&quot;c&quot;&gt;# 2. Enter untrusted container&lt;/span&gt;
bash /tmp/enter-oss.sh

&lt;span class=&quot;c&quot;&gt;# 3. In OSS container, test the repo&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; /workspace
git clone https://github.com/suspicious/repo.git
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;repo
./setup.sh  &lt;span class=&quot;c&quot;&gt;# This runs in isolation&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# 4. Monitor terminal will alert on any suspicious changes&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 5. If compromised, reset the container&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;exit&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;# Leave OSS container&lt;/span&gt;
bash /tmp/reset-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;development-tool-testing&quot;&gt;Development Tool Testing&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Test a new development tool safely&lt;/span&gt;
bash /tmp/enter-oss.sh

&lt;span class=&quot;c&quot;&gt;# In OSS container&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-fsSL&lt;/span&gt; https://some-tool.com/install.sh | bash
some-new-tool &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Monitor for:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Modified binaries&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Network connections&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Hidden files&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# - Unexpected processes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;multi-container-workflow&quot;&gt;Multi-Container Workflow&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Terminal 1: Monitoring&lt;/span&gt;
bash /tmp/monitor-containers.sh

&lt;span class=&quot;c&quot;&gt;# Terminal 2: Trusted work&lt;/span&gt;
bash /tmp/enter-penguin.sh
&lt;span class=&quot;c&quot;&gt;# Do your normal development here&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Terminal 3: Untrusted testing&lt;/span&gt;
bash /tmp/enter-oss.sh
&lt;span class=&quot;c&quot;&gt;# Test random GitHub projects here&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Terminal 4: Status checking&lt;/span&gt;
bash /tmp/status.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;performance-considerations&quot;&gt;Performance Considerations&lt;/h2&gt;

&lt;h3 id=&quot;resource-limits&quot;&gt;Resource Limits&lt;/h3&gt;

&lt;p&gt;The OSS container is intentionally limited to prevent resource abuse:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Current limits (modify as needed)&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.cpu 2        &lt;span class=&quot;c&quot;&gt;# 2 CPU cores max&lt;/span&gt;
lxc config &lt;span class=&quot;nb&quot;&gt;set &lt;/span&gt;oss limits.memory 2GB   &lt;span class=&quot;c&quot;&gt;# 2GB RAM max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;monitoring-overhead&quot;&gt;Monitoring Overhead&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Monitoring script uses minimal resources&lt;/li&gt;
  &lt;li&gt;Scans every 30 seconds (configurable)&lt;/li&gt;
  &lt;li&gt;Focuses on security-critical changes only&lt;/li&gt;
  &lt;li&gt;Can run continuously without impact&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;container-reset-speed&quot;&gt;Container Reset Speed&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Full OSS container reset takes ~2-3 minutes&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;time &lt;/span&gt;bash /tmp/reset-oss.sh
&lt;span class=&quot;c&quot;&gt;# Includes: stop, delete, recreate, configure, install packages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;integration-with-development-workflow&quot;&gt;Integration with Development Workflow&lt;/h2&gt;

&lt;h3 id=&quot;ide-integration&quot;&gt;IDE Integration&lt;/h3&gt;

&lt;p&gt;You can configure your IDE to work with the container setup:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# VS Code with Remote-Containers&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Point to penguin container for trusted development&lt;/span&gt;
code &lt;span class=&quot;nt&quot;&gt;--folder-uri&lt;/span&gt; vscode-remote://attached-container+penguin/path/to/project

&lt;span class=&quot;c&quot;&gt;# For untrusted code testing, always use terminal access&lt;/span&gt;
bash /tmp/enter-oss.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;git-configuration&quot;&gt;Git Configuration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# In penguin (trusted) - normal git config&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;s2&quot;&gt;&quot;Your Name&quot;&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;your.email@domain.com&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# In OSS (untrusted) - minimal or fake config only&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.name &lt;span class=&quot;s2&quot;&gt;&quot;Test User&quot;&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;test@example.com&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Never configure real credentials&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;file-sharing-between-containers&quot;&gt;File Sharing Between Containers&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Share files from trusted to untrusted (one-way only)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Copy from penguin to OSS for testing&lt;/span&gt;
lxc file push /path/in/penguin/container/file.txt oss/workspace/

&lt;span class=&quot;c&quot;&gt;# NEVER copy from OSS to penguin without verification&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Instead, manually recreate verified files in penguin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;extending-the-security-model&quot;&gt;Extending the Security Model&lt;/h2&gt;

&lt;h3 id=&quot;custom-monitoring-rules&quot;&gt;Custom Monitoring Rules&lt;/h3&gt;

&lt;p&gt;Add your own detection rules to the monitoring script:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Example: Monitor for specific file types&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;check_crypto_miners&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;local &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;container&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
    &lt;span class=&quot;nv&quot;&gt;miners&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;lxc &lt;span class=&quot;nb&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$container&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; find /tmp &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*mine*&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-name&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;*crypto*&quot;&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class=&quot;nb&quot;&gt;wc&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$miners&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-gt&lt;/span&gt; 0 &lt;span class=&quot;o&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;then
        &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;RED&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;!!!  Potential cryptocurrency miner detected&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;NC&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;log-integration&quot;&gt;Log Integration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Enhanced logging in monitoring script&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;LOG_FILE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$HOME&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;/container-security.log&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;function &lt;/span&gt;log_alert&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;date&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;+%Y-%m-%d %H:%M:%S&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; ALERT: &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$LOG_FILE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;ChromeOS’s layered container architecture provides an excellent foundation for safely testing untrusted code. This setup gives you:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;True isolation&lt;/strong&gt;: Each container is properly sandboxed&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Real-time monitoring&lt;/strong&gt;: Immediate detection of compromise attempts&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Easy recovery&lt;/strong&gt;: Quick container reset when needed&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Maintained productivity&lt;/strong&gt;: Your trusted environment stays clean&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is using ChromeOS’s existing security model rather than fighting it. By running the monitoring from Termina and isolating untrusted code in its own container, you get enterprise-grade security with developer-friendly workflows.&lt;/p&gt;

&lt;p&gt;All that said, I’m unlikely to use it without the Terminal integration (see below). I’d like to open Terminal then click &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;oss&lt;/code&gt; a line below &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;penguin&lt;/code&gt;.  Instead, I’m probably use a container in podman inside penguin - that seems hardened as a solution and workable today.&lt;/p&gt;

&lt;h2 id=&quot;the-terminal-app-limitation&quot;&gt;The Terminal App Limitation&lt;/h2&gt;

&lt;p&gt;CromeOS’ Terminal app’s inability to add custom container entries is a significant UX gap. 
The fact that only penguin appears as a clickable row means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;No visual distinction between trusted/untrusted environments&lt;/li&gt;
  &lt;li&gt;Can’t theme the OSS terminal differently (red background would be perfect!)&lt;/li&gt;
  &lt;li&gt;Extra friction to access the untrusted container&lt;/li&gt;
  &lt;li&gt;No way to know at a glance which container you’re in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SSH workaround (penguin -&amp;gt; ssh dev@oss-ip) adds complexity to what should be a single click.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Implications&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These UX limitations create real security risks:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Command confusion&lt;/strong&gt;: Without visual distinction, you might accidentally run trusted commands (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; with your real credentials) in the untrusted container&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Increased attack surface&lt;/strong&gt;: The SSH workaround opens network ports and adds authentication complexity that could be exploited&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remote access temptation&lt;/strong&gt;: Lack of remote access means you might be tempted to run untrusted code directly on your primary machine when traveling, defeating the entire security model&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would really want to be able to make a second container from within the Terminal app. The UX hints 
that it should be possible but the feature is not there. A huge shame given how incredibly strong the
dev experience on ChromeBooks (that have enough RAM and SSD).&lt;/p&gt;

&lt;h2 id=&quot;the-chrome-remote-desktop-problem&quot;&gt;The Chrome Remote Desktop Problem&lt;/h2&gt;

&lt;p&gt;This is even more frustrating. A ChromeOS Flex machine effectively becomes an island because, only 
Windows and Mac are &lt;strong&gt;first-class&lt;/strong&gt; choices for destination for Chrome Remote Desktop. HOST-OS Linux 
is a &lt;strong&gt;second class&lt;/strong&gt; choice because one seems to need PhD-level understanding of X11/Wayland/DISPLAY. Multi-user 
possibilities might be one of complexities, and systemd could be on the “complicating” mix too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome Remote Desktop TO ChromeOS/Linux (the very platform Google controls!) is not supported at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yet, Chrome Remote Desktop FROM ChromeOS to Windows or Mac is supported.&lt;/p&gt;

&lt;p&gt;This means you can’t remotely access your secure container (in ChromeOS Flex on a spare PC) from your Chromebook when traveling, defeating much of the purpose of having a dedicated security testing environment.
Potential Workarounds (all imperfect). Or from your Mac or Windows laptop which support Chrome Remoting as an origin just fine.&lt;/p&gt;

&lt;p&gt;For Terminal access I could create a web-based terminal (ttyd or similar) in 
each container, accessible via different ports, but I’d rather be in my terminal of choice: Terminal&lt;/p&gt;

&lt;p&gt;I have other VNC server in penguin or Guacamole, too, but I wish this were a mainstream feature of ChromeOS once you’ve enabled developer features.&lt;/p&gt;

&lt;p&gt;The irony is that Google has all the pieces (Crostini, Chrome Remote Desktop, Terminal app) but hasn’t connected them properly. A simple “Add Container to Terminal” button and proper Chrome Remote Desktop support would solve everything.
It’s particularly galling because ChromeOS is supposed to be the “simple, secure” option, yet these limitations push us toward complex workarounds that probably decrease security.&lt;/p&gt;

&lt;h2 id=&quot;googlers&quot;&gt;Googlers&lt;/h2&gt;

&lt;p&gt;And if any Googlers have got this far: can you have an explicit “Disable trackpad while typing” setting as macOS Sierra had. It was removed after sierra. Chromebooks have plastic chassis and mild weight adjacent to the trackpad while typing can cause a click at current pointer position.&lt;/p&gt;

&lt;h2 id=&quot;updates&quot;&gt;Updates&lt;/h2&gt;

&lt;p&gt;Jan 2026: The “Multi-container” ChromeOS flag &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#crostini-multi-container&lt;/code&gt; has moved from a hidden experiment to a core feature then deprecated. Now there’s a neg &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;baugutte&lt;/code&gt; 
direction that is containerless - wee VMs instead. Its early days with that, and I’m playing with &lt;a href=&quot;https://github.com/aldur/nixos-crostini/&quot;&gt;nixos-crostini&lt;/a&gt; toward the same goals.&lt;/p&gt;
</content>
 </entry>
 
 
</feed>