Mert
OPPO Find N6 Tükiye’de: İşte Cep Yakan Fiyatı!
Three weeks after I said CLAUDE.md writes itself, it added 4 more rules without me
A thesis, three weeks later
On April 28th, I published an article on DEV.to that made four claims about a CLAUDE.md file — the one that constrains the coding agent at each session — and ended with this sentence: “the CLAUDE.md is never finished, and that’s precisely why it works” (4 incidents, 4 rules: how my CLAUDE.md wrote itself). That was a thesis, not a metaphor. Three weeks passed. The file added four rules without me.
What I mean is that I didn’t write them on a day I sat down to write rules. I received them on the days an incident had produced them, and all I had to do was record them before they evaporated in the flow of the project. The difference, on paper, seems thin. In the practice of a solo dev piloting an agent in production, it’s doctrinal.
One clarification before the list: this article’s title almost said “five rules.” live-snapshot-cache.md was committed on April 25th, three days before the pivot article was published. It doesn’t count. I’d rather have the honest number than the comfortable rounding.
The audit, measured by git
No narrative without raw material. Here is what git log --diff-filter=A --follow on .claude/rules/ returns between April 28th 2026 (publication of the pivot article) and May 21st 2026 (today) — four new files strictly post-publication.
cache-auth-contract.md — committed May 2nd. Born from a technical debt audit, not a production crash. It’s a Friday late afternoon. Niran is settled two desks away, headphones on, a closed burger box in the corner. I’m going through docs/dette/AUDIT-2026-04-30.md section D-20 on the right screen, code on the left. Reading through getCachedFormateurs, I understand that the unstable_cache is shared across all users — session not propagable. If someone exposes this function via an API route without a guard, it’s a silent RBAC leak. I look up to talk to Niran about it. He removes his headphones, listens, says “Yeah, that bites.” He puts them back on. The rule gets written that evening.
// .claude/rules/cache-auth-contract.md — anti-pattern to prohibit
// Flaw: no guard
export async function GET() {
const formateurs = await getCachedFormateurs()
return Response.json(formateurs)
}
// Correct
export async function GET(req: NextRequest) {
const supabase = await createSupabaseServer()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
const profile = await getUserProfile(user)
if (!canAccess(profile, 'communication')) return new Response('Forbidden', { status: 403 })
const formateurs = await getCachedFormateurs()
return Response.json(formateurs)
}
inscrit-nom-prenom-required.md — committed May 14th. “Hm, it’s buggy.” — Catherine, two hours earlier. “But it’s a quick fix.” The daily drift probe sonde_contacts_orphelins_inscrits surfaced an inscrit-status contact with an empty first name — a child named Loubna, imported from Airtable where the first name lived in a separate unmapped column. The grep that followed found sixteen similar cases. What would have broken regular attendance tracking (Cannot read properties of undefined) gets caught by a Postgres CHECK constraint that closes the incident class at the root.
-- .claude/rules/inscrit-nom-prenom-required.md
CHECK (
statut <> 'inscrit'
OR (
nom IS NOT NULL AND nom <> ''
AND prenom IS NOT NULL AND prenom <> ''
)
)
Without this CHECK, the rule stays textual in CLAUDE.md and the next import brings back a seventeenth case before the next probe. With it, the INSERT fails, and the import surfaces the problem at the source.
contrat-formation.md — committed May 16th, in the wake of ADR-0068. It’s the longest rule, because the professional training contract is a Snapshot where every column carries its guarantee of immutability. motivation_code, text_version, cases_cochees, pdf_storage_path — frozen at generation, never recalculated retroactively. An evolution of the contract is never a rewrite of the Snapshot, it’s a new event with a new text_version. The rule exists because the three-year Qualiopi audit rests entirely on the immutability of the generated PDF and the associated trainee signature — a retroactive recalculation would be enough to make the file indefensible.
hybrid-snapshot-live-reset.md — committed May 19th, two days before this article. Before sending the fifty-three Phase 2 re-enrollment SMS messages, a pre-flight audit surfaced that one token out of the fifty-three was consumed — created in test mode that morning, clicked, used_at non-null. If the Phase 2 SMS went out as-is, the link /r/<short_code> would return a 410 Gone, the contact loses their conversion, the support ticket lands at end of day. The generateTokenForContact helper was resurrecting the object (frozen identity Snapshot) but forgetting to reset the Live usage marker. Fix commit 07ed02d. The rule names the pattern, sets it against R6 Live / Snapshot / Cache of the toolkit, of which it is the project-specific extension.
The rule that couldn’t have been written before
Let’s go back to the second rule, Catherine’s and Loubna’s. Sure, I could have, on March 21st 2026 the day the first CLAUDE.md was created, abstractly written “an enrolled contact must have a first and last name”. But that rule wouldn’t have held, because it would have been read, nodded at, and would never have produced a Postgres CHECK constraint. The rule that holds isn’t the moral statement, it’s the material mechanism — the audit SQL that must always return zero, the migration that closes the incident class at the root.
-- DB coherence audit — must always return 0
SELECT COUNT(*) FROM contacts
WHERE statut = 'inscrit'
AND ((nom IS NULL OR nom = '') OR (prenom IS NULL OR prenom = ''));
To produce that SQL, you needed the incident. To write the Why paragraph of the rule (which names the sixteen patched contacts, the Airtable origin, the probe that surfaced case-zero), you had to have lived through the widening. No “I write my doctrine on day one” produces that level of specificity. The rule isn’t a drafted precept, it’s a hardened scar.
Four observations about sedimentation
Four things become visible when you line up the four rules and look at them from a distance.
First, none of the four could have been written before their incident. Not because I lacked imagination on March 21st, but because the material precision of a useful rule comes from the encounter with a concrete case. A CHECK constraint, a tunnel mapping to DREETS paper boxes, a used_at reset in the same transaction as the SELECT — details that operate, that abstraction would never have produced.
Second, all of them cite a commit, a migration, a session log, or an ADR. No floating rules. I learned this traceability in the pivot article, but I hadn’t measured it as a mechanism. Today I do: if the rule doesn’t carry its material anchor, it doesn’t do its job. The agent can read it, the human reader too, and both can trace back to the incident if needed.
Third, three out of four prohibit an anti-pattern, the fourth freezes a Snapshot. The negative rule dominates, exactly as #20 predicted. The abstract positive rule (use Server Components by default) gets read and forgotten. The anchored negative rule (a getOr* helper that returns a Snapshot without resetting Live markers silently introduces a dead link at the next reuse) gets read and remembered because it carries its consequence.
Fourth, and this is the observation that changes the status of the CLAUDE.md, I didn’t invent these rules, I received them. The distinction sounds rhetorical, it isn’t. Inventing a rule assumes you imagine it then write it. Receiving a rule assumes an incident produced it and you record it before it evaporates. In the first regime, the file is a solitary writing act that claims exhaustiveness. In the second, the file is a sedimentation device that demands maintaining a holding space — an open notebook, a regular review session, a git log grep at short intervals. The work is no longer to write, it’s to catch.
Why it works now
Whatever we think about documentation best practices, a CLAUDE.md written in one sitting at project start doesn’t hold. It ages in two weeks, it accumulates rules that nobody invokes, and the agent ends up reading it mechanically without loading it into working memory. The CLAUDE.md that holds doesn’t age — it sediments. Each incident deposits its layer, the file carries the material memory of the project rather than its imagined documentation.
Three weeks of post-#20 practice materially confirm the pivot article’s thesis. But they add a nuance that #20 hadn’t formulated: for the file to sediment, there must be something that solicits it. A daily drift probe, a monthly debt audit, a sending pre-flight that asks “are all fifty-three tokens actually active?” — these are the devices that produce the incidents that produce the rules. Without them, the file stays on its naive day-one version, and the project drifts in silence.
Coda
A CLAUDE.md that no longer writes itself is a file that no longer works — either because the project is dead, or because the devices that solicit the incident have disappeared. Three weeks after publishing the thesis, I can verify it on raw material: four rules, four incidents, four Why paragraphs that couldn’t have been written before. The file kept writing itself while I was doing something else. But I also know now what keeps it alive — and what would be enough to kill it if I stopped paying attention.
If you maintain a CLAUDE.md on a project where you’re piloting an agent in production, ask yourself the material question: what has it added without you in the last three weeks? If the answer is nothing, it’s probably not the doctrine that’s run dry. It’s the device that produces incidents that’s gone dark.
Sequel to 4 incidents, 4 rules: how my CLAUDE.md wrote itself (April 28th, 2026). Measurements at 23 days’ distance, verified on .claude/rules/ of the Rembrandt repo — 18 rule files currently active, 4 strictly post-pivot. No Counterpart Toolkit in this sequel — that topic lives in a separate series.
Why On-Device AI Is Quietly Winning Over Cloud Inference — Three Reasons You Didn’t See Coming
I noticed something odd a few months ago. Several engineers I respect — people building serious AI pipelines, not hobbyists — quietly shifted from API-based inference back toward running models locally. Not because of some principled stance. Not because they read a blog post. Because they hit real problems and local inference solved them faster than any API change could.
Nobody announced this. There was no “local AI is back” wave on Twitter. It just… happened.
That got me thinking: if experienced engineers are making this choice in silence, the reasons probably aren’t the ones being loudly debated. It’s not “privacy is important” in the abstract. It’s specific, concrete pain points that don’t make good conference talks but absolutely dictate engineering decisions.
Here are the three that actually moved the needle.
Reason 1: The Regulatory Pressure Nobody Talks About Openly
Everyone vaguely knows that GDPR exists. Fewer people have internalized what it means when your AI system processes user data through a third-party cloud endpoint.
When you send a user’s screen content, text input, or behavioral data to a cloud inference API, you’ve just created a data transfer to a third-party processor. Under GDPR Article 28, that processor needs a Data Processing Agreement. Under GDPR Chapter V, if that server is outside the EU, you need Standard Contractual Clauses or an adequacy decision. Under China’s PIPL, cross-border data transfer requires a government-filed security assessment for anything above certain thresholds.
This is not hypothetical. GDPR enforcement has been escalating steadily — the Irish DPC alone fined Meta €1.2 billion in May 2023 for EU-US data transfer violations. CCPA enforcement in California continues to expand. China’s Personal Information Protection Law (PIPL), in effect since November 2021, is tightening cross-border data transfer requirements with mandatory security assessments.
Here’s the trap developers fall into: your AI vendor’s privacy policy is not your compliance shield.
When your application sends data to an inference API and something goes wrong, regulators look at you — the data controller — not the API provider. The fact that the API provider has good security practices is relevant but not sufficient. You still need to demonstrate lawful basis, purpose limitation, data minimization, and cross-border transfer compliance for every single inference call that processes personal data.
For applications involving GUI automation, document processing, customer service interactions, or anything that touches user-generated content — that’s basically every inference call.
Running inference on-device eliminates this exposure cleanly. The data never leaves the user’s hardware. There’s no cross-border transfer. The DPA requirement with an AI vendor disappears. The compliance surface collapses dramatically.
I’ve watched legal teams add 3-6 months to product timelines trying to untangle the regulatory implications of cloud inference for EU or China deployments. On-device inference sidesteps the entire conversation. For teams that ship to regulated markets, that timeline compression is worth a lot.
[IMAGE: A diagram showing data flow comparison — cloud inference with multiple regulatory checkpoints (GDPR, CCPA, PIPL) vs. on-device inference where data stays local]
Reason 2: Latency Isn’t Just About Speed — It’s About Determinism
The average latency numbers for cloud inference look reasonable. Sub-200ms for most major providers, often well under 100ms for smaller models. When someone benchmarks cloud inference, those are the numbers they publish.
The number that actually matters for production systems is P99. Or even P99.9.
Cloud inference latency is variable in ways that are difficult to predict and nearly impossible to bound. A 50ms average can have a 2000ms P99 due to cold starts, regional capacity fluctuations, network path changes, or provider-side throttling. This isn’t a criticism of cloud providers — it’s inherent to shared infrastructure at scale.
For many applications, this variability is fine. A chatbot that occasionally takes 2 seconds instead of 0.2 seconds is annoying but functional.
For GUI automation agents, variability kills reliability.
When an agent is navigating a UI — clicking buttons, reading screen state, deciding what to do next — it’s executing a feedback loop. Each inference call determines the next action, which changes the screen state, which feeds back into the next inference call. The entire loop depends on predictable timing. If one inference step takes 20x longer than expected, the agent may be acting on stale screen state, may miss UI transitions, or may time out waiting for an action to complete.
This isn’t a latency optimization problem. It’s a determinism problem. The agent needs to be able to reason about timing as part of its control logic.
On-device inference gives you P99 you can actually plan around. On Apple Silicon with appropriate quantization, you get consistent throughput that’s bounded by local hardware — not by whatever is happening on a shared inference cluster on the other side of the planet. You can profile it, characterize it, and build your agent’s timing assumptions around real measurements.
For GUI automation specifically, the reliability improvement from this determinism is often more impactful than the raw latency numbers suggest. We’ve observed this pattern repeatedly: switching from cloud inference to on-device inference doesn’t just make an agent faster — it makes it work in scenarios where it was previously failing intermittently and unpredictably.
[IMAGE: A latency distribution graph comparing cloud inference (wide spread, long tail) vs. on-device inference (tight distribution, predictable P99)]
Reason 3: The Cost Crossover Most People Missed
This one requires some arithmetic, but it’s worth doing.
Cloud inference pricing has been dropping steadily. For context, GPT-4-class inference that cost $0.03/1K tokens in 2023 is now available at a fraction of that from multiple providers. For many use cases, cloud inference is cheap.
But “cheap per call” and “cheap at scale” are different calculations.
Three things happened in the last 18 months that changed the math for on-device inference:
First: W4A8 and W8A8 quantization techniques matured significantly. A model running W4A8 quantization on Apple Silicon achieves quality within a few percentage points of full-precision while running at dramatically higher throughput. This isn’t theoretical — it’s in production, measurable, and reproducible.
Second: Apple M4 silicon arrived with a substantially improved Neural Engine and memory bandwidth profile. A 4B quantized model on Apple Silicon now achieves throughput that would have required a much larger machine a year ago.
Third: The “zero marginal cost” nature of on-device inference becomes meaningful at enterprise scale.
Here’s the calculation people miss: for applications where inference is happening continuously — monitoring, automation agents, real-time assistance — the cost per hour of cloud inference adds up in a way that the per-call pricing obscures.
If you’re running an autonomous agent that makes 10 inference calls per minute during active use, and a user is active for 6 hours per day, that’s 3,600 inference calls per day per user. At even $0.001 per call (which is optimistic for capable models), that’s $3.60/user/day — $1,314/user/year. For a B2B product with 500 users, you’re looking at $657,000/year in pure inference costs, scaling linearly with usage.
The break-even against on-device depends on hardware costs and usage patterns, but for enterprise deployments with heavy inference usage, the crossover typically arrives in 12-18 months. After that point, every inference call is essentially free.
This doesn’t mean on-device always wins on cost — for bursty, low-volume use cases, cloud inference is clearly more economical. But for continuous-use automation and monitoring applications, the TCO calculation has quietly flipped, and many teams haven’t updated their mental model to account for it.
What This Means for Builders
None of this means cloud inference is going away. Cloud inference will remain the right choice for many workloads — burst capacity, the largest models, multi-modal tasks that require more than local hardware can provide, and anywhere the regulatory and latency considerations I’ve described don’t apply.
But the decision is no longer “cloud by default, local if you’re weird about privacy.” The calculus is more nuanced now:
- If you process personal data from users in the EU, California, or China, you need to do the compliance math honestly before assuming cloud inference is viable.
- If you’re building agent loops where timing matters, P99 latency from cloud inference may be silently causing reliability failures you’re attributing to other causes.
- If you have sustained, high-volume inference at enterprise scale, you may be past the cost crossover already and not realize it.
The engineers I mentioned at the start didn’t arrive at local inference through ideology. They arrived through debugging. They found the compliance lawyers, the intermittent timeouts, the bills that didn’t look right.
That’s usually how actual engineering decisions get made.
A Project Worth Watching
One example of this shift playing out in practice: Mano-P, an open-source GUI-VLA agent from MiningLamp Technology that runs fully on-device (Apache 2.0, GitHub).
The performance numbers are interesting as a concrete data point for what on-device inference can actually deliver today: Mano-P 1.0-4B running on Apple M5 Pro (64GB, Cider SDK) achieves ~80 tokens/s decode with W8A16 quantization; enabling W8A8 activation quantization speeds up prefill by ~12.7%. The 72B evaluation configuration (not open-sourced — used for benchmarking only) reached 58.2% on the OSWorld benchmark (proprietary model category). The open-source 4B version is what developers actually deploy and run locally.
If you’re building in the GUI automation or edge agent space and want to see what current hardware can actually do, it’s worth a look:
brew tap Mininglamp-AI/tap && brew install mano-cua
[IMAGE: Screenshot of Mano-P running an on-device GUI task on a MacBook, showing the agent interface and live task execution]
The quiet shift I noticed among those engineers isn’t a trend piece. It’s just people solving real problems with the best available tools — and the best available tools for a growing set of problems now happen to run locally.
That’s worth paying attention to.
Trois semaines après avoir dit que mon CLAUDE.md s’écrivait tout seul, il a ajouté 4 règles sans moi
Une thèse, trois semaines plus tard
Le 28 avril, j’ai publié sur DEV.to un article qui affirmait quatre choses à propos d’un fichier CLAUDE.md — celui qui contraint l’agent de codage à chaque session — et qui finissait par cette phrase : « le CLAUDE.md n’est jamais fini, et c’est précisément pour ça qu’il marche » (4 incidents, 4 règles : comment mon CLAUDE.md s’est écrit tout seul). C’était une thèse, pas une métaphore. Trois semaines ont passé. Le fichier a ajouté quatre règles sans moi.
Je veux dire par là que je ne les ai pas écrites le jour où je me suis assis pour écrire des règles. Je les ai accueillies les jours où un incident les avait produites, et où je n’avais plus qu’à les noter avant qu’elles s’évaporent dans la marche du projet. La différence, sur le papier, paraît mince. Dans la pratique d’un dev solo qui pilote un agent en production, elle est doctrinale.
Une précision avant d’entrer dans la liste : le titre de cet article a failli dire « cinq règles ». live-snapshot-cache.md a été commité le 25 avril, trois jours avant la publication de l’article-pivot. Elle ne compte pas. Je préfère l’honnêteté du chiffre exact au confort de l’arrondi.
Le bilan, mesuré par git
Pas de récit possible sans la matière brute. Voici ce que git log --diff-filter=A --follow sur .claude/rules/ retourne entre le 28 avril 2026 (publication de l’article-pivot) et le 21 mai 2026 (aujourd’hui) — quatre fichiers nouveaux strictement post-publication.
cache-auth-contract.md — committé le 2 mai. Né d’un audit de dette technique, pas d’un crash en prod. C’est un vendredi en fin d’après-midi. Niran est posé à deux bureaux de là, casque sur les oreilles, une boîte de burgers fermée dans l’angle. Je parcours docs/dette/AUDIT-2026-04-30.md section D-20 sur l’écran de droite, le code sur l’écran de gauche. En relisant getCachedFormateurs, je comprends que le cache unstable_cache est mutualisé entre tous les utilisateurs — session non propagable. Si quelqu’un expose cette fonction via une route API sans guard, c’est une fuite RBAC silencieuse. Je lève la tête pour en parler à Niran. Il retire son casque, écoute, dit « Ah oui, ça mord. » Il remet le casque. La règle est rédigée ce soir-là.
// .claude/rules/cache-auth-contract.md — anti-pattern à interdire
// Faille : pas de guard
export async function GET() {
const formateurs = await getCachedFormateurs()
return Response.json(formateurs)
}
// Correct
export async function GET(req: NextRequest) {
const supabase = await createSupabaseServer()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
const profile = await getUserProfile(user)
if (!canAccess(profile, 'communication')) return new Response('Forbidden', { status: 403 })
const formateurs = await getCachedFormateurs()
return Response.json(formateurs)
}
inscrit-nom-prenom-required.md — committé le 14 mai. « Hum, ça bug. » — Catherine, deux heures avant. « Mais c’est vite corrigé. » La sonde quotidienne sonde_contacts_orphelins_inscrits a remonté un contact statut='inscrit' avec le prénom vide — l’enfant Loubna, importé d’un Airtable où le prénom vivait dans une colonne séparée non mappée. Le grep qui suit relève seize cas similaires. Ce qui aurait pété l’émargement régulier (Cannot read properties of undefined) tombe dans une CHECK constraint Postgres qui ferme la classe à la racine.
-- .claude/rules/inscrit-nom-prenom-required.md
CHECK (
statut <> 'inscrit'
OR (
nom IS NOT NULL AND nom <> ''
AND prenom IS NOT NULL AND prenom <> ''
)
)
Sans cette CHECK, la règle reste textuelle dans CLAUDE.md et l’import suivant ramènera un dix-septième cas avant la prochaine sonde. Avec, l’INSERT échoue, l’import remonte le problème à la source.
contrat-formation.md — committé le 16 mai, dans le sillage d’ADR-0068. C’est la règle la plus longue, parce que le contrat de formation professionnelle est un Snapshot dont chaque colonne porte sa garantie d’immutabilité. motivation_code, text_version, cases_cochees, pdf_storage_path — figés à la génération, jamais recalculés rétroactivement. Une évolution du contrat n’est jamais une réécriture du Snapshot, c’est un nouvel événement avec une nouvelle text_version. La règle existe parce que l’audit Qualiopi trois ans repose entièrement sur l’immutabilité du PDF généré et de la signature stagiaire associée — un recalcul rétroactif suffirait à rendre le dossier indéfendable.
hybrid-snapshot-live-reset.md — committé le 19 mai, deux jours avant cet article. Avant l’envoi des cinquante-trois SMS de Phase 2 réinscription, un audit pré-flight remonte qu’un token sur les cinquante-trois est consommé — créé en mode test le matin, cliqué, used_at non null. Si le SMS Phase 2 part tel quel, le lien /r/<short_code> renvoie un 410 Gone, le contact perd sa conversion, le ticket support tombe en fin de journée. Le helper generateTokenForContact ressuscitait l’objet (Snapshot d’identité figé) mais oubliait de reset le marker Live d’usage. Fix commit 07ed02d. La règle nomme le pattern, l’oppose à R6 Live / Snapshot / Cache du toolkit dont elle est l’extension projet-spécifique.
La règle qui n’aurait pas pu être écrite avant
Reprenons la deuxième règle, celle de Catherine et de Loubna. Certes, j’aurais pu, le 21 mars 2026 jour de création du premier CLAUDE.md, écrire abstraitement « un contact inscrit doit avoir un nom et un prénom ». Mais cette règle-là n’aurait pas tenu, parce qu’elle aurait été lue, hochée de la tête, et n’aurait jamais produit une CHECK constraint Postgres. La règle qui tient n’est pas l’énoncé moral, c’est le mécanisme matériel — le SQL d’audit qui doit toujours retourner zéro, la migration qui ferme la classe d’incident à la racine.
-- audit DB de cohérence — doit toujours retourner 0
SELECT COUNT(*) FROM contacts
WHERE statut = 'inscrit'
AND ((nom IS NULL OR nom = '') OR (prenom IS NULL OR prenom = ''));
Pour produire ce SQL, il fallait l’incident. Pour rédiger le paragraphe Pourquoi de la règle (qui mentionne par leur nom les seize contacts patchés, l’origine Airtable, la sonde qui a remonté le cas-zéro), il fallait avoir traversé l’élargissement. Aucun « j’écris ma doctrine au jour 1 » ne produit ce niveau de spécificité. La règle n’est pas un précepte rédigé, c’est une cicatrice durcie.
Quatre constats sur la sédimentation
Quatre choses se voient quand on aligne les quatre règles et qu’on les regarde de loin.
Premièrement, aucune des quatre n’aurait pu être écrite avant son incident. Pas parce que je manquais d’imagination le 21 mars, mais parce que la précision matérielle d’une règle utile vient de la rencontre avec un cas concret. Une CHECK constraint, un mapping tunnel vers cases papier DREETS, un reset de used_at dans la même transaction que le SELECT — autant de détails opérants que l’abstraction n’aurait jamais produits.
Deuxièmement, toutes citent un commit, une migration, un session log ou un ADR. Aucune règle flottante. Cette traçabilité, je l’ai apprise dans l’article-pivot, mais je ne l’avais pas mesurée comme une mécanique. Aujourd’hui je la mesure : si la règle ne porte pas son ancrage matériel, elle ne fait pas son travail. L’agent peut la lire, le lecteur humain aussi, et l’un comme l’autre peuvent remonter à l’incident en cas de doute.
Troisièmement, trois sur quatre interdisent un anti-pattern, la quatrième fige un Snapshot. La règle négative domine, exactement comme #20 le prédisait. La règle positive abstraite (utilisez Server Components par défaut) se lit et s’oublie. La règle négative ancrée (un helper getOr* qui retourne un Snapshot sans reset des markers Live introduit silencieusement un lien dead à la prochaine réutilisation) se lit et se retient parce qu’elle porte sa conséquence.
Quatrièmement, et c’est le constat qui change le statut du CLAUDE.md, je n’ai pas inventé ces règles, je les ai accueillies. La distinction paraît rhétorique, elle ne l’est pas. Inventer une règle suppose qu’on l’imagine puis qu’on l’écrit. Accueillir une règle suppose qu’un incident l’a produite et qu’on la note avant qu’elle s’évapore. Dans le premier régime, le fichier est un acte d’écriture solitaire qui prétend à l’exhaustivité. Dans le second, le fichier est un dispositif de sédimentation qui exige de tenir un sas — un cahier ouvert, une session de bilan régulière, un grep git log à intervalle court. Le travail n’est plus d’écrire, c’est d’attraper.
Pourquoi ça marche maintenant
Quoi que nous puissions penser des bonnes pratiques de documentation, un CLAUDE.md rédigé d’un trait au démarrage d’un projet ne tient pas. Il vieillit en deux semaines, il accumule des règles que personne ne convoque, et l’agent finit par le lire mécaniquement sans en charger la mémoire opérante. Le CLAUDE.md qui tient, lui, ne vieillit pas — il sédimente. Chaque incident dépose sa couche, le fichier porte la mémoire matérielle du projet plutôt que sa documentation imaginée.
Trois semaines de pratique post-#20 confirment matériellement la thèse de l’article-pivot. Mais elles ajoutent une nuance que #20 n’avait pas formulée : pour que le fichier sédimente, encore faut-il qu’il y ait quelque chose qui le sollicite. Une sonde drift quotidienne, un audit dette mensuel, un pré-flight d’envoi qui demande « est-ce que les cinquante-trois tokens sont bien actifs ? » — ce sont ces dispositifs qui produisent les incidents qui produisent les règles. Sans eux, le fichier reste sur sa version naïve du jour 1, et le projet dérive en silence.
Coda
Un CLAUDE.md qui ne s’écrit plus est un fichier qui ne fonctionne plus — soit parce que le projet est mort, soit parce que les dispositifs qui sollicitent l’incident ont disparu. Trois semaines après avoir publié la thèse, je peux la vérifier sur la matière brute : quatre règles, quatre incidents, quatre paragraphes Pourquoi qui ne pouvaient être rédigés avant. Le fichier a continué de s’écrire pendant que je faisais autre chose. Mais je sais aussi, maintenant, ce qui le maintient vivant — et ce qui suffirait à le tuer si je le perdais de vue.
Si vous tenez un CLAUDE.md sur un projet où vous pilotez un agent en production, posez-vous la question matérielle : qu’a-t-il ajouté sans vous depuis trois semaines ? Si la réponse est rien, ce n’est probablement pas la doctrine qui s’est tarie. C’est le dispositif qui produit les incidents qui s’est éteint.
Suite de 4 incidents, 4 règles : comment mon CLAUDE.md s’est écrit tout seul (28/04/2026). Mesures à 23 jours d’écart, vérifiées sur .claude/rules/ du repo Rembrandt, 18 fichiers de règles projet actuellement, 4 strictement post-pivot. Pas de Counterpart Toolkit dans cette séquelle — ce sujet vit dans une autre série.
Building a P2P ‘Wikipedia for Machines’: Verifiable RAG with the Holepunch Stack
Current LLMs hallucinate when they lack context. While Retrieval-Augmented Generation (RAG) helps, existing pipelines force a tough compromise: you either trust centralized search APIs, scrape the live web (which is slow, fragile, and bloated with SEO spam), or maintain heavy, complex crawling infrastructure yourself.
More importantly, none of these methods give you cryptographic proof that the content your LLM is citing actually came from the source you claim.
To solve this, I’ve been building HIVE—a decentralized, peer-to-peer knowledge base designed to be consumed by LLMs, not humans. Think of it as a “Wikipedia for machines” that is completely P2P, cryptographically verifiable, and has no central authority.
The Architecture: BEEs and Queens
Instead of building a monolithic crawler, HIVE splits the workload into two distinct peer topologies using a pure Holepunch stack (Hypercore + Hyperswarm). There is no shared coordination ledger or Autobase; it relies entirely on single-writer logs.
[ Wikipedia / arXiv / RSS ]
│
▼ (Autonomous Extraction & Signing)
┌─────────┐
│ BEE │ (Producer Node - Low Power)
└────┬────┘
│
▼ (Hyperswarm DHT / P2P Replication)
┌─────────┐
│ QUEEN │ (Consumer Node - Heavy Indexer)
└────┬────┘
│
▼ (Semantic Vector Search)
[ Qdrant ] ──► [ Local LLM / API Synthesis ]
- BEEs (Producer Nodes)
BEEs are lightweight, Raspberry Pi-friendly nodes. They autonomously crawl and extract knowledge from structured sources (currently heavy on Wikipedia, with initial arXiv and RSS support).
Every extracted text fragment is ed25519-signed by the BEE.
The signed fragments are appended to the node’s local, single-writer Hypercore feed.
Other nodes can only replicate this feed as read-only.
- Queens (Consumer Nodes)
Queens are the heavy indexer and query nodes.
They discover active BEEs via the Hyperswarm DHT.
They replicate all discovery feeds entirely over P2P—there is zero HTTP traffic between nodes.
As they ingest the data, they verify the ed25519 signatures to guarantee data integrity.
The fragments are then indexed into a local Qdrant vector database to serve semantic queries.
When an LLM queries a Queen, it receives signed, source-traceable fragments with absolute cryptographic proof of origin.
Try It In 3 Minutes (Docker Compose)
If you want to spin up the full stack locally—including a BEE node, a Queen indexer, Qdrant, and a reverse proxy—you can run it via Docker:
git clone https://github.com/capybarist/hive.git && cd hive
cp .env.example .env
nano .env # Paste your LLM API Key (e.g., a free Gemini key from aistudio.google.com)
docker compose up -d
Queen Query Endpoint: Open http://localhost to test the AI synthesis.
BEE Dashboard: Open http://localhost:8080 to monitor autonomous extraction activity.
Minimal Path (~150MB)
If you just want to run a lightweight BEE node to contribute to the network without a heavy vector indexing endpoint:
git clone https://github.com/capybarist/hive.git && cd hive
npm install
echo "LLM_PROVIDER=gemini" > .env
echo "LLM_API_KEY=your_key" >> .env
bash hive.sh
Current State & Honest Limitations (v0.7.0)
HIVE is functional, but it is not battle-tested at a massive scale yet. Here is exactly where the project stands today:
What Works: Full Bee/Queen role splitting, native Hypercore replication over Hyperswarm DHT, automated signature verification on block receipt, and a working Wikipedia forager with a persistent BFS crawl queue.
The Bottlenecks: The extraction rate is highly dependent on your local hardware or your LLM provider’s rate limits (e.g., Groq free tier yields ~6k fragments/day, while an Ollama CPU setup does around ~600/day). Additionally, Hyperswarm DHT can still be finicky behind aggressive corporate firewalls.
Roadmap: What’s Next (v0.7.x)
Source-Driven Extraction: Transitioning from a static topic tree to per-BEE source declarations (Wikipedia, arXiv, Common Crawl).
Unified ForagerSource Interface: Making it so adding a new data source only requires implementing a single interface file.
Common Crawl Integration: Indexing the open web via reproducible, immutable snapshots instead of live scraping.
Score-by-Corroboration: Implementing an algorithm to boost content confidence when multiple independent BEEs extract identical facts from different sources.
Looking for Feedback
HIVE is completely open-source (BUSL, automatically converting to MIT in 4 years). There is no startup behind this, no VC funding, and absolutely no token economy or cryptocurrency attached. I built it because I needed verifiable local RAG and couldn’t find an existing solution.
I would love to hear from developers working on P2P architectures, distributed systems, or self-hosted RAG:
The Topo Split: Does the Bee/Queen separation feel sound to you, or do you see architectural edge cases that could lead to unexpected bottlenecks?
Open Web Ingestion: Is leveraging Common Crawl snapshots a solid approach for reproducible web ingestion, or are the local storage overheads too punishing for self-hosted nodes?
The P2P Commons: Would you dedicate spare home-lab compute or bandwidth to seed/produce signed knowledge fragments for a shared network?
Let me know your thoughts or hit me with some tough architectural critique in the comments!
📂 Source Code: https://github.com/capybarist/hive
📖 Manifesto: https://github.com/capybarist/hive/blob/main/MANIFESTO.md
🏗️ Technical Deep-Dive: https://github.com/capybarist/hive/blob/main/CLAUDE.md
One Command, One Working Kubernetes Cluster! Building My Daily-Driver Lab on OrbStack.
Part 2 of 7 — The Mac Kubernetes Lab: A Production-Mirror Setup from Scratch.
Previously in Part 1: I walked through why I replaced Multipass with OrbStack, the dual-cluster architecture I settled on, and a preview of the M1 vs M4 CNI problem that’s coming in Part 4.
The cluster I am going to set up in this article is the one I spend most of my working day inside. It’s a single-node Kubernetes cluster, always on, idles at around 512 MB of memory, has real LoadBalancer IPs and wildcard DNS out of the box. No MetalLB, /etc/hosts editing,kubectl port-forward muscle memory. By the time this article is done, it will also have Istio, Vault, and Crossplane running. Total elapsed time, the first time you do it: about ten minutes.
If you’ve ever built a local Kubernetes cluster and then spent the next twenty minutes wiring up MetalLB and editing /etc/hosts so you can actually reach a service from a browser, this is going to feel almost suspicious.
# 💻 Mac
orb start k8s
# Confirm cluster is udr
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# orbstack Ready control-plane,master 30s v1.33.x
kubectl config current-context
# orbstack
That’s all! No kubeadm, no CNI configuration, no certificate management. The cluster is up and reachable in under thirty seconds the first time, and instantly on every subsequent start.
What makes OrbStack’s networking special?
This is where OrbStack genuinely earns its keep. On a typical local cluster; kind, minikube, kubeadm, LoadBalancer services stay in pending state until you install MetalLB on OrbStack:
- LoadBalancer services get a real, reachable IP automatically.
- *.k8s.orb.local wildcard DNS resolves from your Mac browser, with no /etc/hosts entry.
- cluster.local DNS also resolves from your Mac.
- Every service type is reachable without kubectl port-forward.
⚠️ The wildcard DNS only resolves on your Mac. Other devices on your network won’t see *.k8s.orb.local. If you need a service reachable from another machine, that’s what Cluster 2 (Parts 3–6) is for.
Installing Istio via Helm.
I use Helm rather than istioctl for two reasons.
First, it’s how I manage Istio on the production EKS clusters at work, so the muscle memory transfers.
Second, Helm gives fine-grained control over resource requests, which matters on a laptop.
# 💻 Mac
kubectx orbstack
# Add helm charts
helm repo add istio https://istio-release.storage.googleapis.com/charts
helm repo update
# Step 1 - Base CRDs
helm install istio-base istio/base
--namespace istio-system --create-namespace
--set defaultRevision=default
# Step 2 - Control plane
# The PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION flag is required on OrbStack
# to prevent DNS resolution conflicts with the host network.
helm install istiod istio/istiod
--namespace istio-system
--set pilot.env.PILOT_ENABLE_WORKLOAD_ENTRY_AUTOREGISTRATION=true
--set global.proxy.resources.requests.cpu=10m
--set global.proxy.resources.requests.memory=64Mi
--wait
# Step 3 - Ingress gateway
# OrbStack assigns a real LoadBalancer IP automatically - no MetalLB needed.
helm install istio-ingress istio/gateway
--namespace istio-ingress --create-namespace
--set service.type=LoadBalancer
# Verify the gateway got an EXTERNAL-IP
kubectl get svc -n istio-ingress
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# istio-ingress LoadBalancer 10.x.x.x 198.19.x.x 80:xxx/TCP
# Enable sidecar injection on the default namespace
kubectl label namespace default istio-injection=enabled
Gateways and Virtual Services on the native cluster.
This is the moment OrbStack feels like cheating. You create a Gateway pointing at *.k8s.orb.local, and it just works from your Mac browser. No IP lookups. No /etc/hosts, no 127.0.0.1:8080 proxying.
How the traffic actually flows.
The Gateway resource binds to the istio-ingress LoadBalancer service, OrbStack intercepts traffic to the *.k8s.orb.localwildcard domain at the Mac level and routes it to that LoadBalancer IP. The Virtual Service then routes inside the cluster to the right service.
Example 1: Basic routing with httpbin:
# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: httpbin
spec:
replicas: 1
selector:
matchLabels:
app: httpbin
template:
metadata:
labels:
app: httpbin
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: httpbin
spec:
selector:
app: httpbin
ports:
- port: 80
targetPort: 80
---
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: httpbin-gateway
spec:
selector:
istio: ingress
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "httpbin.k8s.orb.local"
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: httpbin
spec:
hosts:
- "httpbin.k8s.orb.local"
gateways:
- httpbin-gateway
http:
- route:
- destination:
host: httpbin
port:
number: 80
EOF
Open http://httpbin.k8s.orb.local in your Mac browser. It works immediately.

Example 2: Traffic splitting (canary):
The same pattern used in production canary deployments:
# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-split
spec:
hosts:
- "myapp.k8s.orb.local"
gateways:
- myapp-gateway
http:
- route:
- destination:
host: myapp-v1
port:
number: 80
weight: 80
- destination:
host: myapp-v2
port:
number: 80
weight: 20
EOF
Example 3: Path-based routing:
Route /v1 and /v2 to different backend services:
# 💻 Mac
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: path-routing
spec:
hosts:
- "api.k8s.orb.local"
gateways:
- api-gateway
http:
- match:
- uri:
prefix: /v1
route:
- destination:
host: api-v1-svc
port:
number: 80
- match:
- uri:
prefix: /v2
route:
- destination:
host: api-v2-svc
port:
number: 80
EOF
One OrbStack gotcha: don’t enable httpsRedirect!
Do not use httpsRedirect: true in a Gateway on the native cluster. OrbStack intercepts LoadBalancer traffic in a way that causes an infinite 301 redirect loop when TLS redirect is enabled.
# ❌ This breaks on OrbStack native K8s
servers:
- tls:
httpsRedirect: true
# ✅ Use plain HTTP on the native cluster
servers:
- port:
number: 80
protocol: HTTP
For TLS testing, use Cluster 2 (the VM cluster, coming in Part 3), where you have full control over the network stack.
Installing the rest of the daily stack:
Vault in dev mode.
Dev mode trades durability for speed. No unsealing, no persistence concerns, instant startup. It’s the right call for the daily-driver cluster where I’m testing AppRole workflows, PKI policies, or Kubernetes auth configurations, and the value is in iteration speed, not data preservation.
# 💻 Mac
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm install vault hashicorp/vault
--namespace vault --create-namespace
--set "server.dev.enabled=true"
kubectl get pods -n vault
# vault-0 1/1 Running 0 30s
For production-grade Vault with HA Raft storage, use Cluster 2 (Part 3). Dev mode here is intentional — it trades durability for speed.
Crossplane:
Crossplane turns the Kubernetes cluster into a universal control plane for cloud infrastructure. I use it heavily with the AWS provider at work.
# 💻 Mac
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
# Note: --enable-composition-functions was removed in newer versions.
# Composition Functions are enabled by default now.
helm install crossplane crossplane-stable/crossplane
--namespace crossplane-system --create-namespace
kubectl get pods -n crossplane-system
Stop/start without losing your work.
# 💻 Mac
# Stop - releases all CPU and RAM
orb stop k8s
# Start - full state persists (deployments, services, secrets, configmaps)
orb start k8s
# Verify
kubectx orbstack
kubectl get nodes
kubectl get pods -A
The native cluster state persists across stop/start. Vault dev mode data is lost on restart by design, but everything else, Crossplane installations, Istio configuration, and your workload deployments come back exactly as provisioned.
Quick reference for this cluster:
What’s next.
The daily-driver cluster is the easy half of this lab. The hard half is the one that mirrors production, a real multi-node kubeadm cluster running on four VMs, with HashiCorp Vault as its certificate authority.
That’s where Part 3 picks up: creating the VMs, wiring their networking, and standing up a 3-tier Vault PKI that will sign every certificate in the cluster.
← Part 1: Why I Replaced Multipass with OrbStack.. | Part 3: Building a Production-Grade Vault PKI for a Local kubeadm Cluster Without the Shortcuts →
I’m Noah Makau, a DevSecOps engineer based in Nairobi. I run a small DevOps consultancy and hold CKA, CKAD, and AWS Solutions Architect Professional certifications, currently preparing for CKS. I write about Kubernetes, Vault, Crossplane, and the day-to-day of running platforms that actually have to stay up.
Originally published at blog.arkilasystems.com.
npm Supply Chain Audit: The Checklist Most Teams Stop Too Early
Originally posted on getcommit.dev.
In October 2021, ua-parser-js was used by Facebook, Microsoft, Amazon, and Google. It had 7 million weekly downloads. It had no reported CVEs. It had clean code and an active maintainer. Every security tool in the npm ecosystem reported: nothing wrong here.
Then the maintainer’s npm token was compromised. A malicious release deployed a cryptominer and credential stealer to every CI pipeline and production server that ran npm install that day. The blast radius: four hours, millions of installs, Fortune 500 pipelines.
The same structural profile — single maintainer, massive download volume, clean code — was present before the attack. It was visible, computable from public data, and nobody was measuring it.
This is the gap most npm supply chain audits leave open.
The three-layer model
A complete npm supply chain audit covers three distinct layers. Most teams run one. Sophisticated teams run two. Almost nobody runs all three — because the third layer only became a standard practice after real-world attacks validated the signal.
| Layer | Question answered | Tools | Timing |
|---|---|---|---|
| 1 — Known vulnerabilities | Does this version have a documented CVE? | npm audit, Snyk | After discovery |
| 2 — Code-level anomalies | Is this package doing something suspicious right now? | Socket | At publish time |
| 3 — Structural risk | Is this package a high-value attack target before any attack occurs? | proof-of-commitment | Before the event |
Each layer is essential. Each one fails at what the others cover. A supply chain audit that uses only one or two is a partial audit.
Layer 1: Known vulnerability scanning
What it does
npm audit submits your dependency tree to GitHub’s Advisory Database and returns matches. Snyk extends this with a proprietary database, license compliance checking, and auto-fix pull requests. Both tools answer the same question: does this package version have a documented, reported vulnerability?
What it catches
Prototype pollution vulnerabilities. Known RCE bugs. Packages with published CVEs where the fix is a version bump. The entire category of “known bad” code.
What it misses
Anything that isn’t documented yet. A CVE requires discovery, analysis, and reporting — that process takes days to weeks after an attack. For the ua-parser-js attack, npm audit returned zero for the four hours the malicious version was live, and returned zero for every day before that, including the years when the structural risk was building.
Checklist: Layer 1
- Run
npm auditin CI on every push. Fail on high/critical. - Set a
.npmrcaudit-level threshold appropriate for your risk tolerance. - If using Snyk: enable the GitHub integration for automated PRs on new CVEs.
- Review
npm audit --jsonoutput for transitive vulnerabilities, not just direct dependencies. - Don’t suppress audit failures with
npm audit --productionif devDependencies run in CI pipelines.
Layer 2: Real-time code analysis
What it does
Socket performs static analysis on the actual published package source — not the CVE database, not the repository. When a new version publishes to the npm registry, Socket analyzes the code for suspicious patterns: dynamic eval of user-controlled strings, network calls to unexpected endpoints, obfuscated payloads, environment variable harvesting.
For the class of attack where a maintainer’s account is compromised and they push a malicious release, Socket catches the payload within minutes of publication — before most CI pipelines would run.
What it catches
Token-theft attacks. Malicious versions with obfuscated payloads. Packages that suddenly start making network calls they didn’t make before. The ua-parser-js attack, run through Socket today, would be flagged by the suspicious network activity and eval patterns in the malicious release.
What it misses
The structural risk that exists before any malicious code is published. Socket analyzes code. The code was clean until the attack happened. Socket correctly reports “nothing suspicious in the code” when the code is genuinely clean — it can’t report on structural conditions that live outside the code.
Socket also doesn’t score blast radius. A solo-maintained package with 400 million weekly downloads gets the same analysis as a solo-maintained package with 400 downloads. The structural risk is orders of magnitude different, and that difference isn’t in the code.
Checklist: Layer 2
- Enable Socket on your GitHub organization. The free tier covers open source.
- Configure Socket to block PRs that introduce packages with medium/high risk signals.
- Subscribe to Socket alerts for packages already in your lockfile.
- Review Socket’s “install scripts” and “network access” flags — these are rarely false positives for utility packages.
Layer 3: Structural risk scoring
What it does
Structural risk scoring answers the question that neither Layer 1 nor Layer 2 can answer: is this package a high-value attack target, right now, based on observable structural conditions?
The structural conditions that define a high-value npm target are three things:
-
Single publish credential. One npm account with publish access. One compromised token = one malicious release to every system that runs
npm install. - High download volume. The blast radius of a compromised publish. Hundreds of millions of weekly installs means CI pipelines and production deployments at Fortune 500 companies.
- Active maintenance. A package that publishes regularly is more valuable to an attacker than an abandoned one, because developers trust it and update to new versions.
All three of these signals are computable from public data. The npm registry exposes maintainer count per package. The npm download API returns weekly statistics. GitHub exposes commit history and contributor count. No scanning required. No proprietary database. The data was always there.
What it catches
The conditions that make a package a rational attack target before any attack has occurred. Event-stream (2018), ua-parser-js (2021), and colors.js (2022) all shared this structural profile: sole publisher, enormous download volume, active releases. The structural risk was computable and present for years before each incident. No existing tool was measuring it.
What it misses
Everything that requires code inspection. Structural scoring tells you about the conditions for an attack, not the attack itself. It generates leading indicators, not proof of compromise. A CRITICAL structural score means “this package is a high-value target” — not “this package is currently compromised.” For that, you need Layer 2.
Checklist: Layer 3
- Run
npx proof-of-commitment --file package-lock.jsonagainst your dependency tree. Note every CRITICAL flag (single maintainer, >10M weekly downloads). - For every CRITICAL package, document the risk in your threat model. Do you have a contingency if this package is compromised? Can you pin to a specific version and monitor for unexpected updates?
- Check Trusted Publishing status for critical dependencies. Packages that publish via OIDC provenance (Trusted Publishing) are meaningfully harder to compromise than packages that publish via personal tokens.
- Re-run quarterly. Package maintainer counts change when projects gain or lose contributors. A package that was safe at multi-maintainer status may drift to single-maintainer over time.
- Evaluate transitive dependencies, not just direct ones. Your direct dependencies may look clean; the packages they depend on may carry CRITICAL flags. The blast radius includes the entire install tree.
The complete audit checklist
Run this in sequence. Each layer surfaces a different class of problem.
Before a major dependency update
# Layer 1: Known vulnerabilities
npm audit --json | jq '.vulnerabilities | length'
# Layer 2: Socket analysis (if CLI installed)
npx @socketsecurity/cli check package.json
# Layer 3: Structural risk
npx proof-of-commitment --file package-lock.json
In CI (on every PR)
# Layer 1
npm audit --audit-level=high
# Layer 3 (structural risk, fail on CRITICAL)
npx proof-of-commitment --file package-lock.json --fail-on-critical
Quarterly review
- Re-run Layer 3 on the full dependency tree. Note any packages that gained CRITICAL status since last quarter.
- Review Socket alert history for your organization. Note patterns.
- Check Trusted Publishing adoption for your five most critical dependencies.
- Update your threat model with any new structural risks.
Why most audits stop at Layer 1 or 2
The argument against Layer 3 is: “It’s a leading indicator, not an exploit. A CRITICAL flag doesn’t mean anything was attacked.”
That’s true. It’s also the argument for credit scoring before fraud detection exists. Leading indicators measure structural conditions that predict future events. They’re not proof of the event — they’re the reason you insure against it before it happens.
The practical objection is noise: if your audit returns CRITICAL flags for minimatch, @types/node, and lodash, what do you do with that? You can’t replace these packages. You can’t wait for them to gain more maintainers.
The answer isn’t to fix the packages. It’s to know the risk exists, document it in your threat model, and make informed decisions about dependency pinning, update monitoring, and response plans. The same way you know that your RDS instance is a single point of failure and set up a read replica anyway.
The ua-parser-js compromise lasted four hours and affected millions of systems because the teams running those systems didn’t have a response plan. They didn’t have one because nobody told them the risk existed. Layer 3 is the tool that tells you the risk exists.
Try the structural layer
proof-of-commitment is open source and zero-install:
# Scan your project
npx proof-of-commitment --file package-lock.json
# Scan specific packages
npx proof-of-commitment axios lodash chalk zod
# pnpm or yarn
npx proof-of-commitment --file pnpm-lock.yaml
npx proof-of-commitment --file yarn.lock
Or use the web interface at getcommit.dev/audit — paste a GitHub repo URL and get structural risk scores for every dependency in seconds.
Layer 1 tells you what broke. Layer 2 tells you what’s breaking now. Layer 3 tells you what will break. You need all three.
Get a daily business report in your inbox every morning — automate it with n8n (free workflow JSON)
Every founder has some version of a morning ritual: open 4 different tabs, check Stripe, check analytics, check the spreadsheet, try to mentally add up whether yesterday was a good day.
That’s 15 minutes every morning. 250+ times a year. 60+ hours you’ll never get back.
Here’s a 5-node n8n workflow that compiles your daily business metrics and delivers a clean formatted report to your inbox (and Slack) at 7 AM — automatically, every single day.
What the workflow does
Node 1 — Daily schedule trigger
Fires every morning at a configurable time. No polling, no manual runs. Set it once, forget it forever.
Node 2 — Google Sheets: read your metrics
Reads your business data from a Google Sheet — sales rows, revenue, order counts. The default expects columns Date, Revenue, Orders but you can reshape the Code node to match whatever you already track.
Node 3 — Code node: calculate KPIs
Processes the raw rows and computes:
- Total revenue for the period
- Order count
- Average order value
- Week-over-week change (yesterday vs same day last week)
The math is plain JavaScript — readable and easy to adjust.
Node 4 — Gmail: send the HTML report
Sends a clean, formatted HTML email with metric cards. Revenue in green, orders in blue, average order in purple, week-on-week trend in orange. You open your email and immediately know if yesterday was good or bad.
Node 5 — Slack: post the summary
Posts a concise text summary to a Slack webhook. Optional — just delete this node if you don’t use Slack.
Full workflow JSON
{
"name": "Daily Business Report Generator",
"nodes": [
{"parameters":{"rule":{"interval":[{"field":"cronExpression","expression":"0 7 * * *"}]}},"id":"dr1","name":"Every Day at 7 AM","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.2,"position":[240,300]},
{"parameters":{"documentId":{"__rl":true,"value":"YOUR_SHEET_ID","mode":"id"},"sheetName":{"__rl":true,"value":"Sales","mode":"name"},"options":{"rangeDefinition":"specifyRangeA1","range":"A:E"}},"id":"dr2","name":"Read Sales Data","type":"n8n-nodes-base.googleSheets","typeVersion":4.5,"position":[460,300]},
{"parameters":{"jsCode":"const rows = $input.all();nconst today = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });nconst sales = rows.map(r => parseFloat(r.json.Revenue || 0));nconst totalRevenue = sales.reduce((a, b) => a + b, 0);nconst orderCount = rows.filter(r => r.json.Revenue && parseFloat(r.json.Revenue) > 0).length;nconst avgOrderValue = orderCount > 0 ? (totalRevenue / orderCount).toFixed(2) : 0;nconst yesterday = parseFloat(rows.slice(-1)[0]?.json?.Revenue || 0);nconst weekAgo = parseFloat(rows.slice(-8, -7)[0]?.json?.Revenue || 0);nconst weekChange = weekAgo > 0 ? (((yesterday - weekAgo) / weekAgo) * 100).toFixed(1) : 'N/A';nreturn [{ json: { date: today, totalRevenue: totalRevenue.toFixed(2), orderCount, avgOrderValue, weekOverWeekChange: weekChange } }];"},"id":"dr3","name":"Calculate KPIs","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300]},
{"parameters":{"sendTo":"[email protected]","subject":"={{ '📊 Daily Report — ' + $json.date }}","emailType":"html","message":"=<html><body style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px'><h1 style='color:#2c3e50'>📊 Daily Business Report</h1><p style='color:#7f8c8d'>{{ $json.date }}</p><table width='100%' cellpadding='12'><tr><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#2ecc71'>${{ $json.totalRevenue }}</div><div style='color:#7f8c8d;font-size:13px'>Revenue</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#3498db'>{{ $json.orderCount }}</div><div style='color:#7f8c8d;font-size:13px'>Orders</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#9b59b6'>${{ $json.avgOrderValue }}</div><div style='color:#7f8c8d;font-size:13px'>Avg Order</div></td><td style='background:#f8f9fa;text-align:center;border-radius:6px'><div style='font-size:26px;font-weight:bold;color:#e67e22'>{{ $json.weekOverWeekChange }}%</div><div style='color:#7f8c8d;font-size:13px'>WoW Change</div></td></tr></table></body></html>"},"id":"dr4","name":"Send Email Report","type":"n8n-nodes-base.gmail","typeVersion":2.1,"position":[900,200]},
{"parameters":{"webhookUri":"YOUR_SLACK_WEBHOOK_URL","text":"={{ '📊 *Daily Report — ' + $json.date + '*n💰 Revenue: $' + $json.totalRevenue + 'n📦 Orders: ' + $json.orderCount + 'n📈 WoW: ' + $json.weekOverWeekChange + '%' }}"},"id":"dr5","name":"Post to Slack","type":"n8n-nodes-base.slack","typeVersion":2.2,"position":[900,420]}
],
"connections": {
"Every Day at 7 AM":{"main":[[{"node":"Read Sales Data","type":"main","index":0}]]},
"Read Sales Data":{"main":[[{"node":"Calculate KPIs","type":"main","index":0}]]},
"Calculate KPIs":{"main":[[{"node":"Send Email Report","type":"main","index":0},{"node":"Post to Slack","type":"main","index":0}]]}
},
"settings":{"executionOrder":"v1"},
"tags":[{"name":"reporting"}]
}
Setup (10 minutes)
- Import the JSON into n8n (New Workflow → Import from clipboard)
-
Create your metrics sheet in Google Sheets with columns:
Date,Revenue,Orders— or adapt the Code node to match your existing data - Connect your Google account in both the Sheets and Gmail nodes (one OAuth connection covers both)
-
Replace
YOUR_SHEET_IDwith your actual sheet ID — it’s in the URL:docs.google.com/spreadsheets/d/[SHEET_ID]/edit -
Replace
[email protected]with your email address in the Gmail node - (Optional) Set up a Slack Incoming Webhook and paste the URL in Node 5, or delete that node entirely
-
Adjust the cron in Node 1 —
0 7 * * *fires at 7 AM UTC. Change7to any hour you prefer. - Activate the workflow — it runs automatically from now on
First run: manually trigger Node 1 to verify it reads your sheet correctly before activating.
Customizations
Multiple data sources
Add more Sheets nodes before the Code node — one sheet for sales, one for support tickets, one for email open rates. In the Code node, access them by index: const salesRows = $input.all() (with a Merge node upstream to combine them).
Pull from Stripe or Shopify directly
Replace the Sheets node with an HTTP Request node hitting the Stripe API (/v1/charges?created[gte]=YESTERDAY_TIMESTAMP) or Shopify Orders API. Same Code node logic applies — just adjust the field names.
Weekly digest instead of daily
Change the cron to 0 8 * * 1 (every Monday at 8 AM). Update the Code node to sum the last 7 rows instead of just yesterday’s data.
Add traffic data
Add an HTTP Request node that calls the Google Analytics Data API for sessions, bounce rate, and top pages from the previous day. Merge it into the Code node alongside your sales data.
Color-coded status (green/red)
In the Code node, add: targetMet: parseFloat($json.totalRevenue) >= YOUR_DAILY_GOAL. In the email HTML, reference it to make the Revenue card green when you hit target and red when you miss.
Real impact
For a solo founder running an online store, this means waking up to a clean summary instead of opening 5 apps. You know within 10 seconds of looking at your phone whether yesterday was good.
For agency owners with multiple clients: add a loop over an array of sheet IDs. One workflow run, one report per client, all in your inbox by 7:05 AM.
The workflow runs on n8n — self-hosted, open source, no monthly per-workflow fees.
Get the full automation bundle
This workflow is part of the FlowKit 15-template n8n bundle — each template covers a different business use case: lead capture, invoice generation, AI customer support, social media automation, price monitoring, appointment reminders, and more.
Grab the full bundle at stripeai.gumroad.com — pre-tested, documented, ready to activate.
Built with n8n. Self-hostable, open source, no vendor lock-in.
Try to Hack My AI-Agent Workflow: GitHub Issues as a Control Surface
I built a small public experiment.
You can open a GitHub Issue and ask an AI-agent workflow to create a static page on my website:
- If your request fits the rules, the workflow may turn it into a published page.
- If your request does not fit the rules, the agents should reject it and explain why.
And if you want to make the experiment more interesting, do not only submit a valid request. Try to bypass the boundary. Try to make the workflow publish something it should not publish. Then inspect the issue comments, labels, pull request, and final result.
This is the game.
But under that game, there is a more serious question:
Can external user intent enter a real product repository and become a bounded, reviewable product change without giving up product ownership?
That is what I am trying to explore.
The public playground is here: Demo Pages
The challenge
The surface rule is simple:
Create a GitHub Issue that requests a small static demo page.
The workflow should read the issue, decide whether the request belongs to the allowed area, run the required agent steps, create or update the page, validate the result, and publish it only if the process succeeds.
But this is not a public CMS.
It is not a hosting service.
It is not a website builder.
It is not a place where an external user should be able to change my homepage, global layout, styles, scripts, product pages, or site structure.
The allowed result is intentionally narrow: a bounded inert static page under the Demo Pages section.
So the challenge has two sides.
First, try the normal path:
Create a small demo page request and see whether the workflow can turn it into a published static artifact.
Then try the adversarial path:
Ask for something outside the boundary and see whether the workflow stops you.
For example, you can try to request a homepage change, inject instructions into the issue body, ask the agent to modify global styles, or try to smuggle forbidden behavior into the generated content.
Please keep the challenge inside the intended boundary. This is a workflow and prompt-boundary experiment, not an infrastructure attack. Do not attack the server, credentials, network, GitHub itself, availability, or anything outside the issue text and the public repository workflow.
The interesting question is not whether the server can be broken.
The interesting question is whether a chain of AI agents can defend a product boundary while still allowing useful delegated work.
What you can inspect
The workflow is intentionally public enough to leave evidence.
A request should not disappear into a private chatbot.
A useful result should leave a visible trail:
- the original issue;
- the labels that describe state transitions;
- comments written by agents;
- a pull request when a change is produced;
- validation evidence;
- the published static page when the change is accepted.
The page itself is not the main artifact.
The main artifact is the trace.
A static page is only the visible end of the process. The more important part is the path from external request to controlled product change.
If the workflow accepts your request, you can inspect what happened.
If the workflow rejects your request, you can inspect why.
A successful rejection is also a useful result.
In ordinary automation, a rejection often feels like failure. In an AI-agent workflow, a correct rejection is evidence that the process has boundaries. It means the agent did not blindly satisfy the user request. It treated the issue as a signal, not as a command.
That distinction matters.
Why I built this
AI coding agents are getting better at producing code.
But code generation is not the hardest part of using agents in real products.
The harder problem is control.
- Who allowed the agent to work?
- What kind of request was considered safe?
- Which files was the agent allowed to change?
- What checks were required?
- What evidence was recorded?
- When should the agent stop and ask for a human decision?
If these questions are not answered, “autonomous agent development” easily becomes an uncontrolled prompt playground.
That may be interesting for demos, but it is not how I want agents to touch real products.
In a product environment, the owner must not lose control. Users can express intent. Agents can perform delegated work. But the product owner must define the paths where autonomous work is allowed, the boundaries where it must stop, and the evidence required before the result enters the product.
This experiment uses static demo pages because they are safe, visible, and easy to understand.
But the real subject is not static page generation.
The real subject is controlled product evolution.
A user opens an issue. The workflow checks whether the request belongs to an approved path. If it does, agents may process it. If it does not, the request should be rejected, blocked, or routed to human review.
That is the model I want to test in public.
The workflow behind the game
The current experiment uses GitHub Issues as the entry point.
A GitHub Issue is a familiar development artifact. It can describe a request, collect discussion, receive labels, trigger events, and preserve history. That makes it a practical control surface for agent-driven work.
The simplified route looks like this:
The workflow does not treat the issue as a direct publishing request.
It treats the issue as a development signal.
That signal must pass through rules.
For the Demo Pages experiment, the allowed area is intentionally small. The agent may work only inside the documented demo-page boundary. It must not turn a reader request into arbitrary site changes.
This is where the workflow becomes more important than the generated page.
A simple page-generation demo could be built with one prompt and one script.
That is not the point here.
The point is to show a controlled route where request admission, agent execution, validation, pull request creation, merge, and publication are separate observable steps.
What protects the boundary
The boundary is not protected by asking the model to “please behave.”
That is not enough.
The workflow needs several kinds of control.
First, there is a documented scope.
The agent must know what kind of request is allowed, what area of the repository may be changed, and what must remain untouched.
Second, there is admission.
Before implementation starts, the request should be classified. A request that targets the homepage, global layout, scripts, styles, or product content should not be treated as a valid demo-page request.
Third, there is restricted execution.
Agents should run in a bounded environment and work through repository changes, not through direct production mutation.
Fourth, there is validation.
The result must be checked before it becomes a published artifact.
Fifth, there is evidence.
Labels, comments, pull requests, and logs matter because they make the process inspectable. Without evidence, autonomy is difficult to trust.
This is why I am inviting people not only to submit valid requests, but also to submit boundary-crossing requests.
A workflow that only succeeds on clean examples is not very interesting.
A workflow becomes more useful when it can say:
No, this request is outside the approved path, and here is why.
That answer is part of the product control model.
Why this may matter for product teams
This experiment looks small because the artifact is small.
A static page is not a large feature.
But the pattern is larger than the artifact.
Many teams already use GitHub Issues, pull requests, CI checks, reviews, and deployment pipelines. AI agents can enter this environment, but they need more than task prompts. They need process boundaries.
A product team does not only need an agent that can write code.
It needs a way to answer:
- which requests can be delegated;
- which requests require human review;
- what evidence must be produced;
- what validation is required;
- how the owner can inspect and change the rules;
- how the product avoids becoming controlled by random external prompts.
That is the commercial reason I care about this.
I am not trying to demonstrate that an AI agent can write a static page. That is already obvious.
I am trying to demonstrate that an AI-agent workflow can transform an external request into a controlled, reviewable, bounded product change.
For a real team, this pattern could apply to documentation fixes, small UI corrections, test additions, generated examples, controlled content updates, internal tools, or other well-bounded change paths.
The important part is not the specific artifact.
The important part is the governance loop:
intent -> boundary check -> delegated work -> evidence -> review -> product change
What powers this experiment
The public workflow is powered by my GitHub Flows work.
The host application is here: github-flows-app
It is a ready-to-run host application around @teqfw/github-flows. It starts a Node.js process, loads local runtime configuration, exposes the GitHub webhook ingress, provides a workspace for configuration and logs, and allows the workflow runtime to react to GitHub events.
The host application itself is not the whole idea.
The broader idea is that repository events can drive agent execution through an explicit process. An issue is opened. A label is added. A pull request is created. A validation result appears. A merge happens. These events can move work through a controlled chain instead of relying on one long manual chat session.
I described the conceptual layer here: Turning User Intent Into Controlled Product Evolution
That article explains the model behind this experiment: users express intent, the product owner defines approved paths, agents act as delegated intermediaries, and the product changes only through bounded, observable, reviewable steps.
Try it
Start here: Demo Pages
Try a simple valid request first.
Then try a request that should be rejected.
Look at the labels.
Read the agent comments.
Follow the pull request if one appears.
Open the published page if the request reaches publication.
If the workflow rejects you, that may be the correct result.
If the workflow publishes something it should not publish, I want to know.
And if you are working with a real product team and thinking about how AI agents could safely participate in GitHub-based development, I am interested in practical cases.
The question is no longer only:
Can an AI agent write code?
The more useful question is:
Can we give agents limited authority inside a process that product owners can control?
If that is the kind of problem you are facing, you can contact me.





