<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jules Robineau</title>
    <description>The latest articles on DEV Community by Jules Robineau (@julesrobineau).</description>
    <link>https://dev.to/julesrobineau</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4033931%2F6c7e37e2-5d4d-486b-81c8-591c267775cd.jpg</url>
      <title>DEV Community: Jules Robineau</title>
      <link>https://dev.to/julesrobineau</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julesrobineau"/>
    <language>en</language>
    <item>
      <title>Every form field your agent will read is a prompt</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 16 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/every-form-field-your-agent-will-read-is-a-prompt-6ol</link>
      <guid>https://dev.to/julesrobineau/every-form-field-your-agent-will-read-is-a-prompt-6ol</guid>
      <description>&lt;p&gt;My site has a contact form. Anyone can type 4,000 characters into it. That text lands in a mailbox.&lt;/p&gt;

&lt;p&gt;Separately, an agent has been reading my freelance offers for months. Both uses raise the same question: what happens when an agent reads a stranger's text?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: researchers demonstrated CRM data exfiltration through a public form. An agent followed instructions hidden in the text. I limit the consequences of a manipulated response: no direct tools, a checked format and separate sending rules. These measures reduce risk. They do not guarantee a truthful answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers who plug an agent into text written by strangers: emails, forms, tickets, reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  SalesBleed: a public form was enough
&lt;/h2&gt;

&lt;p&gt;On 24 September 2026, Zenity Labs published three flaws in Agentforce, Salesforce's agent platform. They named them SalesBleed.&lt;/p&gt;

&lt;p&gt;The attack starts from a Web-to-Lead form. That is the "contact us" form of a website, wired to the CRM. It is public by design: a prospect has no account.&lt;/p&gt;

&lt;p&gt;The attacker fills in the form like a prospect. In one of the fields, they hide a &lt;a href="https://en.wikipedia.org/wiki/Prompt_injection" rel="noopener noreferrer"&gt;prompt injection&lt;/a&gt;. Then they wait.&lt;/p&gt;

&lt;p&gt;One day, an employee asks the agent a routine question. The agent reads the poisoned record. It follows the hidden instructions, fetches other CRM data, and sends it out through an image tag. Nobody clicked on anything.&lt;/p&gt;

&lt;p&gt;Zenity reported the flaws on 1 June. Salesforce fixed them. No real-world exploitation was reported.&lt;/p&gt;

&lt;p&gt;The fix closes that specific case. The pattern exists wherever an agent reads a free-text field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Form validation does not see the problem
&lt;/h2&gt;

&lt;p&gt;My form has all the classic protections. I reread them in the code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A maximum length per field: 100 characters for the name, 4,000 for the message.&lt;/li&gt;
&lt;li&gt;A format check on the email address.&lt;/li&gt;
&lt;li&gt;An invisible trap field that only bots fill in.&lt;/li&gt;
&lt;li&gt;An anti-bot test before sending.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These protections slow down bots and reject some malformed data. None of them reads what the message means.&lt;/p&gt;

&lt;p&gt;An injection is a well-formed message. It has the right length. A patient human wrote it. To the form, it is one more prospect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assume the injection will get through
&lt;/h2&gt;

&lt;p&gt;OWASP ranks prompt injection as the number one risk for applications built on an &lt;a href="https://en.wikipedia.org/wiki/Large_language_model" rel="noopener noreferrer"&gt;LLM&lt;/a&gt;. No filter guarantees that it will block every injection.&lt;/p&gt;

&lt;p&gt;The reason is simple. To a model, everything is text. Your instructions and the stranger's email arrive in the same window. You can write "do not follow instructions in the email". The email can write the opposite, and better than you.&lt;/p&gt;

&lt;p&gt;So I do not ask "how do I block the injection?". I ask this: once the model is convinced, what can it do?&lt;/p&gt;

&lt;p&gt;Here are the four rules of my triage agent. It reads mission offers sent by strangers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 1: the model that reads has no tools
&lt;/h2&gt;

&lt;p&gt;In SalesBleed, the agent reading the record had access to the rest of the CRM. That is what made the leak possible.&lt;/p&gt;

&lt;p&gt;In my setup, the model that reads the email is a local model. My code calls it through a single function. Text goes in, text comes out.&lt;/p&gt;

&lt;p&gt;It cannot search my mailbox, open a URL or send a message through a tool call. But its response feeds the rest of the program. A manipulated grid can influence the action selected next. Having no tools limits direct capabilities, not every downstream consequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 2: check the format without mistaking it for truth
&lt;/h2&gt;

&lt;p&gt;That model has one job: fill in a JSON grid. Stack, rate, remote work, contract type, duration, start date. Each cell gets one of three verdicts: &lt;code&gt;GO&lt;/code&gt;, &lt;code&gt;NO_GO&lt;/code&gt;, &lt;code&gt;UNKNOWN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My Go code checks that each verdict belongs to that list. Here is that format check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;normalizeField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="n"&gt;convstate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;convstate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ToUpper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"GO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"NO_GO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"UNKNOWN"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convstate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convstate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VerdictUnknown&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unknown verdict becomes &lt;code&gt;UNKNOWN&lt;/code&gt;. But a fabricated &lt;code&gt;GO&lt;/code&gt; passes this check. This function validates the vocabulary, not the extracted information.&lt;/p&gt;

&lt;p&gt;The first name reused in the reply also passes through a filter. It rejects digits from 0 to 9 and the characters &lt;code&gt;@,;:&lt;/code&gt;, then keeps one word. Other punctuation remains possible. This cleanup does not guarantee that the result is a first name.&lt;/p&gt;

&lt;p&gt;Code then chooses among predefined actions: ask a question, decline, hand over to me. The model cannot add an action. It can still influence the choice by falsifying the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 3: recheck the criteria you can verify
&lt;/h2&gt;

&lt;p&gt;An injection can also make the grid lie. For example to pass a permanent job off as a freelance mission.&lt;/p&gt;

&lt;p&gt;So on the criteria that matter, my code rereads the raw text. Regular expressions look for unambiguous words: contract type, stack, remote work, an urgent start date.&lt;/p&gt;

&lt;p&gt;If the text says "ASAP" and the model answers "date unknown", the code corrects it. The model suggests, the code decides.&lt;/p&gt;

&lt;p&gt;These checks correct some model errors. They may also catch some manipulated responses, without providing complete protection. Rate and duration, for example, are not recalculated by &lt;code&gt;normalizeField&lt;/code&gt;. And the words in the raw text still come from the sender.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule 4: bound the context
&lt;/h2&gt;

&lt;p&gt;My code cuts the email body at 2,500 bytes. The attachment block is limited to 3,000 bytes. In Go, &lt;code&gt;len(string)&lt;/code&gt; counts bytes. An accented letter can occupy several.&lt;/p&gt;

&lt;p&gt;The trials documented in the code report less reliable verdicts with a limit of 4,000. The limit controls how much content is processed. The current slice can also split a UTF-8 character.&lt;/p&gt;

&lt;p&gt;An injection can fit in one sentence. Shortening the text is therefore not enough to block it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays fragile: the written reply
&lt;/h2&gt;

&lt;p&gt;I will not tell you everything is solved.&lt;/p&gt;

&lt;p&gt;A second model writes the reply. It receives the decision made by the code, plus the original email so it does not ask again for what was already said. So it reads the stranger's text.&lt;/p&gt;

&lt;p&gt;An injection can influence the outgoing content, including adding a commitment or requesting sensitive information. The action category is already chosen, but it does not guarantee what the reply says.&lt;/p&gt;

&lt;p&gt;That is why sending has its own rules, outside the model. A decline always waits for my click. An automatic send is capped and delayed. I detailed those rules in &lt;a href="https://jrobineau.com/en/blog/2026-09-28-ai-agent-auto-reply-approval.html" rel="noopener noreferrer"&gt;the article on automatic replies&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A cap and a delay limit volume, not meaning. Automatic replies remain a risk to assess. Sensitive replies still need human review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Separate what the agent reads from what it is allowed to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] List every free-text field your agent will read: form, email, ticket, attachment&lt;/li&gt;
&lt;li&gt;[ ] Give zero tools to the model that reads text from strangers&lt;/li&gt;
&lt;li&gt;[ ] Make it fill in a fixed format with closed values&lt;/li&gt;
&lt;li&gt;[ ] Validate every value in code, and reject what falls outside the format&lt;/li&gt;
&lt;li&gt;[ ] Recheck the important criteria on the raw text&lt;/li&gt;
&lt;li&gt;[ ] Limit input size without treating that limit as protection against injection&lt;/li&gt;
&lt;li&gt;[ ] Bound actions in code and check the data that triggers them&lt;/li&gt;
&lt;li&gt;[ ] Require human approval for sensitive outgoing messages&lt;/li&gt;
&lt;li&gt;[ ] Block images and links the agent could generate toward the outside&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;As soon as an agent reads what a stranger wrote, the stranger is talking to it. Your contact form becomes a prompt input, whether you like it or not.&lt;/p&gt;

&lt;p&gt;A better prompt can help, but it is not enough. Limit access, check outputs and control sensitive actions. A correctly formatted answer can still be manipulated.&lt;/p&gt;

&lt;p&gt;Plugging an agent into your mailbox, your CRM or your support desk? &lt;a href="https://jrobineau.com/en/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://labs.zenity.io/post/salesbleed-0-click-data-exfiltration-on-agentforce" rel="noopener noreferrer"&gt;Zenity Labs, SalesBleed&lt;/a&gt; · &lt;a href="https://www.theregister.com/security/2026/09/24/salesforce-agentforce-vulns-allowed-0-click-crm-data-theft-anonymous-phishing/5298958" rel="noopener noreferrer"&gt;The Register, 24 September 2026&lt;/a&gt; · &lt;a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" rel="noopener noreferrer"&gt;OWASP, LLM01 Prompt Injection&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>go</category>
      <category>llm</category>
    </item>
    <item>
      <title>GhostAction: a secret deleted from the repo is still in the repo</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 14 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/ghostaction-a-secret-deleted-from-the-repo-is-still-in-the-repo-1cop</link>
      <guid>https://dev.to/julesrobineau/ghostaction-a-secret-deleted-from-the-repo-is-still-in-the-repo-1cop</guid>
      <description>&lt;p&gt;You have committed a password by mistake before. So have I. You deleted it in the next commit and moved on.&lt;/p&gt;

&lt;p&gt;Since October 7, an attack has been looking for exactly those secrets. The ones you think are gone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: the GhostAction campaign is back on GitHub. Its fake workflow no longer steals only the repository secrets. It digs through the whole git history and collects secrets deleted years ago. An assisted scan of 19 repositories found values removed from the current files. Their validity remains unverified. The rule: a committed secret gets revoked. Deleting it does not count.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers and teams who host their code on GitHub, public or private.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GhostAction has been doing since October 7
&lt;/h2&gt;

&lt;p&gt;GhostAction is a credential theft campaign. It targets &lt;a href="https://docs.github.com/en/actions" rel="noopener noreferrer"&gt;GitHub Actions&lt;/a&gt;. It first appeared in September 2025. It came back this week, bigger.&lt;/p&gt;

&lt;p&gt;On October 8, two well-known maintainer accounts were compromised. The attackers swept 345 repositories in two passes of a few minutes, according to StepSecurity. Socket has since counted more than 500 accounts and tens of thousands of repositories.&lt;/p&gt;

&lt;p&gt;The method has three steps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The attacker gains access to a developer's GitHub account. A stolen token is one possible route.&lt;/li&gt;
&lt;li&gt;They push a single file: &lt;code&gt;.github/workflows/security-audit.yml&lt;/code&gt;. The commit message says "Add security audit workflow".&lt;/li&gt;
&lt;li&gt;That workflow audits nothing. It reads the secrets and sends them to the attacker's server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is new in 2026 sits inside the workflow. It downloads the full repository history, then runs &lt;code&gt;git log -p --all&lt;/code&gt;. That command prints every change of every commit, on every branch. The script searches it for keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting a secret does not delete it
&lt;/h2&gt;

&lt;p&gt;Git keeps every version of every file. That is its job. When you remove a line, you add a commit saying "this line no longer exists". The old commit stays.&lt;/p&gt;

&lt;p&gt;In a test repository, add and remove the fictional value &lt;code&gt;DEMO_SECRET_NOT_A_REAL_KEY&lt;/code&gt;. Commit each step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s1"&gt;'DEMO_SECRET_NOT_A_REAL_KEY'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-S&lt;/code&gt; option finds the commits that added or removed that text. Only commit titles and identifiers are shown. Use a fictional value: typing a real secret would copy it into your shell history.&lt;/p&gt;

&lt;p&gt;Until now, many malicious workflows only saw the last commit. By default, the &lt;code&gt;checkout&lt;/code&gt; action downloads only that one. GhostAction adds one line, &lt;code&gt;fetch-depth: 0&lt;/code&gt;, and gets everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a scan of 19 repositories found
&lt;/h2&gt;

&lt;p&gt;While preparing this article, an assistant ran gitleaks on 19 repositories on my machine. This open source tool searches git history for secrets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitleaks git &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--redact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--redact&lt;/code&gt; option hides the values in the output. Without it, you copy your secrets into your terminal and your logs.&lt;/p&gt;

&lt;p&gt;The scan produced 24 findings after deduplication within each repository. The assistant's initial triage identified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;18 findings apparently related to examples, tests or non-secret identifiers.&lt;/li&gt;
&lt;li&gt;6 potentially sensitive values to verify, in password or key fields.&lt;/li&gt;
&lt;li&gt;Among them, 2 password values absent from the current files. They remain in an old Ansible inventory committed in January 2025.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This triage proves neither that the credentials still work nor that anyone used them. Their validity and revocation have not been checked. It shows that a removed value remains accessible in history.&lt;/p&gt;

&lt;p&gt;Private repositories are also affected. A stolen token can read private repositories within its permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revoke first, clean up later
&lt;/h2&gt;

&lt;p&gt;Faced with a secret in the history, the first reflex is to rewrite the history. That is the wrong order.&lt;/p&gt;

&lt;p&gt;Rewriting only touches your copy and the server. Your colleagues' clones keep the old history. So do forks. And if someone already read the secret, they have it.&lt;/p&gt;

&lt;p&gt;GitHub's documentation says the same: the first action is to revoke the secret. A changed password is worth nothing, even if it stays readable. A deleted password that still works is worth everything.&lt;/p&gt;

&lt;p&gt;My order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I revoke or change the secret at the provider.&lt;/li&gt;
&lt;li&gt;I check the provider's logs to see whether it was used.&lt;/li&gt;
&lt;li&gt;I clean the history only if the value still gets in the way. For example if it trips the scanner on every run.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Also change the token that opened the door
&lt;/h2&gt;

&lt;p&gt;StepSecurity stresses a point many teams forget. Changing the stolen secrets is not enough. You must also revoke the GitHub token that pushed the workflow.&lt;/p&gt;

&lt;p&gt;If that token stays valid, the attacker pushes the same file again tomorrow.&lt;/p&gt;

&lt;p&gt;That token is often a &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noopener noreferrer"&gt;PAT&lt;/a&gt; created long ago, with no end date. Go and look at your list. Three simple rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An expiry date on every token.&lt;/li&gt;
&lt;li&gt;Fine-grained tokens, limited to the repositories they need.&lt;/li&gt;
&lt;li&gt;No workflow permission if the tool does not need it. Without it, a token cannot change &lt;code&gt;.github/workflows&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters here. The malicious file is a workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check what your workflows are allowed to read
&lt;/h2&gt;

&lt;p&gt;A workflow pushed to a branch can run from that branch. So protecting &lt;code&gt;main&lt;/code&gt; is not enough.&lt;/p&gt;

&lt;p&gt;Two settings reduce what an unknown workflow can take.&lt;/p&gt;

&lt;p&gt;First, store sensitive secrets in a GitHub environment. An environment can be limited to some branches and require a human approval. A workflow started from an unknown branch cannot reach it.&lt;/p&gt;

&lt;p&gt;Second, replace long-lived cloud keys with &lt;a href="https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt;. You avoid storing a permanent cloud key in GitHub. Still revoke the old keys: migrating does not invalidate them.&lt;/p&gt;

&lt;p&gt;A compromised workflow can obtain temporary access if the cloud authorizes it. Restrict trust conditions to the expected repository, branch or environment. Give the cloud role only the permissions it needs.&lt;/p&gt;

&lt;p&gt;And look for the file itself. This command lists every commit that touched your workflows, on every branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; .github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Stop the next committed secret
&lt;/h2&gt;

&lt;p&gt;A history scan helps find past exposures. For future commits, add a check before committing. Gitleaks has a mode built for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gitleaks git &lt;span class="nt"&gt;--pre-commit&lt;/span&gt; &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nt"&gt;--redact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You plug it into a &lt;code&gt;pre-commit&lt;/code&gt; hook. The commit fails if the scanner detects a secret in the staged changes.&lt;/p&gt;

&lt;p&gt;Keep the scan in the pipeline too. The local hook can be skipped. The pipeline can also be changed or bypassed if its rules allow it. Require it to pass before merging and protect its files. I cover it in &lt;a href="https://jrobineau.com/en/blog/2026-07-29-go-supply-chain-security.html" rel="noopener noreferrer"&gt;my article on securing a Go supply chain&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Treat a committed secret as exposed, even after deleting it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Look for &lt;code&gt;security-audit.yml&lt;/code&gt; in your repositories, on every branch&lt;/li&gt;
&lt;li&gt;[ ] Run &lt;code&gt;gitleaks git . --redact&lt;/code&gt; on every repository&lt;/li&gt;
&lt;li&gt;[ ] Sort the alerts by hand: example, test, or real value&lt;/li&gt;
&lt;li&gt;[ ] Revoke every real value, even old, even deleted&lt;/li&gt;
&lt;li&gt;[ ] Revoke GitHub tokens with no end date&lt;/li&gt;
&lt;li&gt;[ ] Remove the workflow permission from tokens that do not need it&lt;/li&gt;
&lt;li&gt;[ ] Store sensitive secrets in an environment limited to your protected branches&lt;/li&gt;
&lt;li&gt;[ ] Move to OIDC with restricted trust conditions, then revoke old cloud keys&lt;/li&gt;
&lt;li&gt;[ ] Add gitleaks as a &lt;code&gt;pre-commit&lt;/code&gt; hook and a protected pipeline required before merging&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;GhostAction breaks nothing new. It reads what git has always kept. The only answer that holds is to make those old secrets useless.&lt;/p&gt;

&lt;p&gt;Start with one repository, then verify every flagged value. The scan finds candidates. Revocation removes access.&lt;/p&gt;

&lt;p&gt;Want an audit of your repositories and pipelines? &lt;a href="https://jrobineau.com/en/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.stepsecurity.io/blog/ghostaction-returns" rel="noopener noreferrer"&gt;StepSecurity, GhostAction Returns&lt;/a&gt; · &lt;a href="https://socket.dev/blog/ghostaction-cloud-credentials" rel="noopener noreferrer"&gt;Socket, new GhostAction wave&lt;/a&gt; · &lt;a href="https://thehackernews.com/2026/10/credential-stealing-github-actions.html" rel="noopener noreferrer"&gt;The Hacker News, 9 October 2026&lt;/a&gt; · &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository" rel="noopener noreferrer"&gt;GitHub, removing sensitive data from a repository&lt;/a&gt; · &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;gitleaks&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>git</category>
      <category>github</category>
      <category>devsecops</category>
    </item>
    <item>
      <title>Sovereign AI in Europe: what Mistral and OVHcloud already enable, and what is still missing</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 09 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/sovereign-ai-in-europe-what-mistral-and-ovhcloud-already-enable-and-what-is-still-missing-5518</link>
      <guid>https://dev.to/julesrobineau/sovereign-ai-in-europe-what-mistral-and-ovhcloud-already-enable-and-what-is-still-missing-5518</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR.&lt;/strong&gt; Europe already has models and cloud services that let you control several layers of an AI system. Mistral and OVHcloud offer practical options, but dependencies remain. European hosting alone will not keep a service running after a provider cuts access. You need to replace the model, retrieve your data and rebuild the service. A useful test is whether you can sustain that independence for thirty days.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for CTOs, CIOs and teams putting AI features into production.&lt;/p&gt;

&lt;h2&gt;
  
  
  An API can become a continuity risk
&lt;/h2&gt;

&lt;p&gt;An API is the interface your application uses to call a remote service. Losing access means losing the corresponding feature. The cause might be commercial, technical or regulatory. Your continuity plan needs to cover all three.&lt;/p&gt;

&lt;p&gt;On June 12, 2026, Anthropic announced it was suspending Fable 5 and Mythos 5 for all customers. The company cited a US export control directive. It said its other models remained accessible. The statement provides no date for restoring access. &lt;a href="https://www.anthropic.com/news/fable-mythos-access" rel="noopener noreferrer"&gt;[1]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The operational question is straightforward. Can your service continue if a provider or foreign authority suddenly changes access conditions? Yes, for the functions with a prepared and tested replacement. For everything else, continuity remains an assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Europe is not starting from zero
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Available.&lt;/strong&gt; Mistral offers open-weight models and announced general availability of Regional Endpoints on August 11, 2026. These let customers choose Europe or the United States for inference, meaning model execution. Limited, safeguarded transfers to subprocessors outside the selected region remain possible. &lt;a href="https://mistral.ai/news/regional-inference-open-models-new-compute/" rel="noopener noreferrer"&gt;[2]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OVHcloud provides AI Endpoints, an API serving open-weight models from Europe. Its September 28 article also describes OVHai LLM, formed from Dragon LLM following its March acquisition. This lab adds model design and adaptation capabilities to its cloud offering. &lt;a href="https://blog.ovhcloud.com/en/posts/world-summit-ai-2026-sovereign-ai/" rel="noopener noreferrer"&gt;[3]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Planned capacity.&lt;/strong&gt; Mistral targets up to 1 GW by 2030. That is an ambition, not capacity immediately available to reserve. &lt;a href="https://mistral.ai/news/regional-inference-open-models-new-compute/" rel="noopener noreferrer"&gt;[2]&lt;/a&gt; In July, the Commission launched a call for up to seven AI Gigafactories. It also describes a network of nineteen AI Factories, centres for computing and support. Gigafactories are intended to expand that capacity. The call establishes neither delivery nor the commercial access your team will receive. &lt;a href="https://digital-strategy.ec.europa.eu/en/news/eu-launches-ai-gigafactories-call-boost-europes-computing-capacity-and-unlock-more-eu30-billion" rel="noopener noreferrer"&gt;[4]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These components make a European AI stack credible. They offer separate choices of model, operator and region. They do not prove that the resulting system will survive an interruption. That evidence has to come from your architecture.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://jrobineau.com/en/blog/2026-07-27-sovereign-eu-self-hosting.html" rel="noopener noreferrer"&gt;article on sovereign self-hosting&lt;/a&gt; covers the infrastructure foundations. Here, the additional challenge is preserving a useful AI function when the model or operator changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit five layers, not a label
&lt;/h2&gt;

&lt;p&gt;In practice, sovereign AI preserves choices over deployment, data and future changes. Those choices must remain usable when a partner withdraws. Each needs a separate assessment with explicit limits.&lt;/p&gt;

&lt;p&gt;Legal sovereignty concerns applicable laws and the entities controlling the service. Technical portability measures what you can move. Operational autonomy describes what your team can run independently. Industrial independence concerns the ability to produce and replace the components the system needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Model and licence: can you keep what you run?
&lt;/h3&gt;

&lt;p&gt;Weights are the parameters a model learns during training. Downloadable weights let you retain a copy. Their availability alone does not make the entire model open source. Your rights still depend on its licence.&lt;/p&gt;

&lt;p&gt;For your chosen Mistral model, retain the exact version, required files and licence text. Have the rights to operate, adapt and redistribute it checked against your intended use. A brand does not imply one licence across its entire product range.&lt;/p&gt;

&lt;p&gt;Prepare an evaluation set containing representative requests and acceptance criteria. It should measure the errors that matter to your business. A replacement can follow the expected protocol while producing unusable answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inference, cloud and region: who can cut access?
&lt;/h3&gt;

&lt;p&gt;Calling a European API still delegates operations to a third party. Deploying Mistral on OVHcloud therefore requires an explicit choice of operating model. Using a model from a managed catalogue differs from running its weights yourself on reserved resources.&lt;/p&gt;

&lt;p&gt;For the managed option, check the exact model and the terms governing its continued availability. For self-hosting, validate hardware compatibility and your team's ability to operate it. Do not assume every Mistral model is available through every OVHcloud offering.&lt;/p&gt;

&lt;p&gt;Map the contracting entity, region, subprocessors and support access. Add request limits and guaranteed capacity. A second API from the same operator may share the same cause of interruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. GPUs and hardware: separate operation from replacement
&lt;/h3&gt;

&lt;p&gt;A GPU is a processor that performs many calculations in parallel. Hosting non-European accelerators in a European facility does not change their industrial origin. Dependencies can include chips, the software controlling them and replacement parts.&lt;/p&gt;

&lt;p&gt;An installed, autonomous server does not necessarily need a remote API for every answer. It may still become difficult to repair or expand. That risk has a different time horizon from an immediate access outage.&lt;/p&gt;

&lt;p&gt;Document the minimum hardware your fallback model requires. Measure throughput on that configuration. Something that works on your laptop may fail under realistic demand.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Data, identity and monitoring: the model needs its surroundings
&lt;/h3&gt;

&lt;p&gt;Documents must remain exportable together with their access permissions. Retain model instructions and configuration versions too. Vector search represents documents as sequences of numbers to retrieve similar content.&lt;/p&gt;

&lt;p&gt;Replacing the model that produces those representations may require rebuilding the search index. Keep the original documents and measure the time needed to rebuild it. Exporting numbers alone does not guarantee reusable search.&lt;/p&gt;

&lt;p&gt;Then check authentication, secrets and logs. Secrets are the keys and credentials services use. Observability brings together the signals needed to understand how those services behave. Failover must preserve authorized access, alerts and incident evidence.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Operations and exit: can someone else take over?
&lt;/h3&gt;

&lt;p&gt;My multi-cloud experience with Terraform and Ansible leads me to prioritize reproducible infrastructure. These tools describe resources and configuration in files. They help rebuild systems, but providers still have their own requirements. Migration requires adaptations that have actually been tested.&lt;/p&gt;

&lt;p&gt;Keep deployment files, required software and procedures outside the provider boundary you are testing. Ask someone who did not write the procedure to rebuild the service. Record every unexpected intervention and the time it takes.&lt;/p&gt;

&lt;p&gt;Are Mistral and OVHcloud enough to make a stack sovereign? No, choosing them does not resolve all five layers. They can underpin a controlled architecture if rights, dependencies and operations support that choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Europe's remaining limits require funded decisions
&lt;/h2&gt;

&lt;p&gt;Hardware dependencies outside Europe remain a limitation. For a thirty-day plan, focus on accessible capacity and critical spare parts. Over several years, include hardware renewal and software alternatives. These horizons call for different investments.&lt;/p&gt;

&lt;p&gt;Capacity and funding create another constraint. A data centre announcement does not reserve any resources for your company. Ask for a date, region, resource allocation and contractual commitment. Fund the fallback before you need it.&lt;/p&gt;

&lt;p&gt;Tool maturity needs checking feature by feature. Test structured responses, document processing and calls to external tools. Also assess error diagnosis and recovery. A feature appearing in a catalogue does not establish that it meets your application's requirements.&lt;/p&gt;

&lt;p&gt;Skills matter as much as machines. Identify who handles updates, security and incidents. If only one person can maintain the system, autonomy remains fragile. Arrange cover and allocate time for training.&lt;/p&gt;

&lt;p&gt;A fallback model may also trail your primary model on particular tasks. No overall benchmark ranking settles your use case. Explicitly accept a narrower feature set, or retain human review for sensitive answers. The aim is a useful, safe service during the transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thirty-day test
&lt;/h2&gt;

&lt;p&gt;Choose a primary provider and define exactly what losing it would remove. A model outage differs from losing the entire cloud account. Specify essential functions, expected load and acceptable errors. Set the maximum recovery time and acceptable data loss too.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Data export.&lt;/strong&gt; Retrieve documents, permissions, configurations and required history. Import them into the replacement system. Check freshness and access controls.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Model replacement.&lt;/strong&gt; Run the evaluation set against the fallback. Measure quality, response time and cost. Approve the functions it preserves.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Abstraction layer.&lt;/strong&gt; Isolate provider calls in a small component. Test errors and response formats. Keep provider-specific behaviour out of unrelated application code.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Reproducible infrastructure.&lt;/strong&gt; Rebuild the service from versioned files. Retrieve software without the unavailable provider. Verify the capacity actually allocated.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Secret rotation.&lt;/strong&gt; Replace keys, revoke old access and check permissions. Retain a controlled emergency access path.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Observability.&lt;/strong&gt; Recover logs, alerts and quality measurements after failover. Simulate an incident and verify that alerts reach the team.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Backup.&lt;/strong&gt; Restore an independent, encrypted copy. Verify access to decryption keys. Check the result with the users concerned.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Real failover exercise.&lt;/strong&gt; Block primary access in an isolated, representative environment. Run on the fallback for thirty days. Record incidents and interventions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with a short exercise to fix obvious problems. It prepares you for the thirty-day test but does not replace it. Include a restart, a restoration and a configuration change during that period. Check infrequent jobs and expiring credentials too.&lt;/p&gt;

&lt;p&gt;The report should state which functions survived, at what cost and with which team. Record unavailable functions as well. Repeat the exercise after a major model or infrastructure change. Past success does not automatically validate the next version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal is choice, not self-sufficiency
&lt;/h2&gt;

&lt;p&gt;You can buy foreign technology while retaining a workable exit. You can also choose European providers and remain dependent on a service you cannot replace. The difference lies in the evidence available for each layer.&lt;/p&gt;

&lt;p&gt;Mistral and OVHcloud provide foundations for building that capability in Europe. Your task is to turn those options into an architecture the team can operate. Start with one critical function and a failover exercise. Tested reversibility is worth more than a label.&lt;/p&gt;

&lt;p&gt;To examine these dependencies in your system, contact me for an &lt;a href="https://jrobineau.com/en/contact.html" rel="noopener noreferrer"&gt;AI architecture or continuity audit&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://www.anthropic.com/news/fable-mythos-access" rel="noopener noreferrer"&gt;Anthropic, June 12, 2026&lt;/a&gt; · &lt;a href="https://mistral.ai/news/regional-inference-open-models-new-compute/" rel="noopener noreferrer"&gt;Mistral AI, August 11, 2026&lt;/a&gt; · &lt;a href="https://blog.ovhcloud.com/en/posts/world-summit-ai-2026-sovereign-ai/" rel="noopener noreferrer"&gt;OVHcloud, September 28, 2026&lt;/a&gt; · &lt;a href="https://digital-strategy.ec.europa.eu/en/news/eu-launches-ai-gigafactories-call-boost-europes-computing-capacity-and-unlock-more-eu30-billion" rel="noopener noreferrer"&gt;European Commission, July 30, 2026&lt;/a&gt; Accessed October 2, 2026.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>devops</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Taking Over Legacy Go When Nobody Can Review You</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/taking-over-legacy-go-when-nobody-can-review-you-2aj1</link>
      <guid>https://dev.to/julesrobineau/taking-over-legacy-go-when-nobody-can-review-you-2aj1</guid>
      <description>&lt;p&gt;A company hands you a Go codebase nobody in-house can read. The team is strong in its main language, but Go is an island. And the code runs in production, every night, with a customer consuming the API.&lt;/p&gt;

&lt;p&gt;I am in that situation right now. Import tools that read partner files, scheduled batches, an API consumed by a third party. Zero possible reviewer. For every line I change, nobody can tell me it is wrong.&lt;/p&gt;

&lt;p&gt;You can survive it. But not on instinct. With a protocol.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: without a reviewer, your safety net is the existing behaviour. You capture it before touching anything: characterization tests, golden files for the imports, a frozen contract for the API. CI becomes the reviewer. Every commit explains its why to someone who does not read Go. And documentation is a deliverable, not a bonus.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers inheriting code with no safety net, and for CTOs handing a Go island to a freelancer. The protocol is written for a closed perimeter: tools, batches, one API. The end of the article covers what changes when the island is a microservice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The situation is more common than people admit
&lt;/h2&gt;

&lt;p&gt;A company picks Go for one precise need: a fast import tool, a service that must hold the load. One developer builds it, often alone. Then they leave, or change teams. The code stays, it works, and nobody dares touch it. Until the day a change is needed.&lt;/p&gt;

&lt;p&gt;That is not the company's fault. It is the normal life cycle of a technical island. The problem is the moment someone has to go back in, without the author's eyes, without anyone's eyes.&lt;/p&gt;

&lt;p&gt;My rule number one: the danger is not in the code you are going to write. It is in what you are going to break without knowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day 1: the map before the territory
&lt;/h2&gt;

&lt;p&gt;Before reading a single function, I list. The entry points, with &lt;code&gt;go list ./...&lt;/code&gt; and the &lt;code&gt;main&lt;/code&gt; packages. The dependencies, with &lt;code&gt;go mod graph&lt;/code&gt;. The scheduled jobs, with their times. What comes in, what goes out, and who consumes what.&lt;/p&gt;

&lt;p&gt;Then the tools that read for me. &lt;code&gt;go vet&lt;/code&gt; and &lt;a href="https://staticcheck.dev/" rel="noopener noreferrer"&gt;staticcheck&lt;/a&gt; for the obvious mistakes. &lt;a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck" rel="noopener noreferrer"&gt;govulncheck&lt;/a&gt; for known vulnerabilities in dependencies. They do not replace a reviewer, but they never get tired.&lt;/p&gt;

&lt;p&gt;That first day produces no line of code. It produces one page: what the system does, and where it hurts. The tech lead can read that page, even without Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Characterization tests: capture what is, not what should be
&lt;/h2&gt;

&lt;p&gt;A characterization test does not check that the code is right. It records what it does today. The term comes from Michael Feathers, in "Working Effectively with Legacy Code". The idea is twenty years old and has not aged a day.&lt;/p&gt;

&lt;p&gt;For an import tool, that is a golden file. A real input file, anonymized. The output file the tool produces today, stored as is under &lt;code&gt;testdata/&lt;/code&gt;. The test replays the import and compares byte for byte.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rewrite golden files on purpose"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestImportGolden&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;goldenCases&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// one case per testdata/*.in&lt;/span&gt;
        &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;goldenPath&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"testdata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;".golden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goldenPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="n"&gt;o644&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// go test -update&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;goldenPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;got&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatalf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"behaviour changed for %s:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If today's output contains a bug, the golden file contains the bug. On purpose. The day you fix it, the test goes red, you update the golden file in the same commit, and the diff shows exactly what changed. A documented bug beats a surprise bug.&lt;/p&gt;

&lt;p&gt;For a command-line tool, &lt;a href="https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript" rel="noopener noreferrer"&gt;testscript&lt;/a&gt; does the same thing at shell level. A script that runs the command with input files, and compares the output. The full behaviour, flags included, captured in ten lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze the API contract before touching anything
&lt;/h2&gt;

&lt;p&gt;The API is consumed by a third party. That is the point where a regression becomes an incident at someone else's place. I freeze it before the first change.&lt;/p&gt;

&lt;p&gt;Concretely, one test per route that records the current response: status code, useful headers, body, for a real set of requests. Same golden principle, over HTTP, with &lt;code&gt;httptest&lt;/code&gt;. Then I derive a contract document in &lt;a href="https://www.openapis.org/" rel="noopener noreferrer"&gt;OpenAPI&lt;/a&gt; format, from what the code really does, not from what people believe.&lt;/p&gt;

&lt;p&gt;I get that document validated by the API's consumer. That is the only reviewer that counts for this part: the one who breaks if I am wrong. Once validated, the contract is a border. Everything inside is frozen. Everything outside, I can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI is your reviewer
&lt;/h2&gt;

&lt;p&gt;With no human to review, CI has to say no in my place. It runs on every push: build, &lt;code&gt;go vet&lt;/code&gt;, &lt;code&gt;staticcheck&lt;/code&gt;, the golden tests, the contract tests, &lt;code&gt;govulncheck&lt;/code&gt;. And &lt;code&gt;go mod tidy -diff&lt;/code&gt;, so no dependency moves silently.&lt;/p&gt;

&lt;p&gt;It runs before I have written a single line of feature. The order is what matters: CI first, code second. A CI that arrives later only protects the end of the project.&lt;/p&gt;

&lt;p&gt;And deployments never happen from my laptop. From CI, from a tag, with a rollback written before the first production release. The first deployment of code you do not know is prepared like an operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the review you cannot receive
&lt;/h2&gt;

&lt;p&gt;The tech lead does not read Go. But they read plain language, and they know the business better than I do. So every PR starts with a paragraph they can review. What it changes for the user, what it does not change, and how I verified it.&lt;/p&gt;

&lt;p&gt;Small commits, one subject per commit. A refactor never mixes with a feature. A bug found along the way becomes a note and a test, not a fix slipped into another diff.&lt;/p&gt;

&lt;p&gt;Decisions that commit the future, I write in a short file, one per page: the context, the option chosen, what I ruled out. That is an &lt;a href="https://adr.github.io/" rel="noopener noreferrer"&gt;ADR&lt;/a&gt;. In six months, whoever takes over will know why, not just what.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I refuse to do
&lt;/h2&gt;

&lt;p&gt;No rewrite. Code that has run for years is right about details I do not know yet. Replacing it means losing those details all at once.&lt;/p&gt;

&lt;p&gt;No framework change, no dependency bumps for comfort. Only security updates, and one at a time.&lt;/p&gt;

&lt;p&gt;No opportunistic cleanup. An ugly function that works stays ugly until a real need touches it. Every change must be justified by a requested evolution or a red test. Without that, it is free risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What if the island is a microservice?
&lt;/h2&gt;

&lt;p&gt;Everything above assumes a closed perimeter. Files in, files out, one API. A microservice has no such luck. Its behaviour is what it exchanges with its neighbours: HTTP or gRPC calls, messages on a queue, a shared database. The protocol stays. Every step changes shape.&lt;/p&gt;

&lt;p&gt;The map no longer comes from &lt;code&gt;go list&lt;/code&gt;. It comes from production traces and logs: who calls this service, whom it calls, which message topics it reads and writes. A service whose neighbours you do not know is not mapped.&lt;/p&gt;

&lt;p&gt;The golden file becomes a recording of exchanges. You capture real requests and real responses from the neighbouring services, anonymized, and replay them against stand-ins. Heavy dependencies, database or queue, run in test containers with &lt;a href="https://golang.testcontainers.org/" rel="noopener noreferrer"&gt;Testcontainers&lt;/a&gt;. The test then says: for these inputs, the service produced these calls and these messages, in this order.&lt;/p&gt;

&lt;p&gt;The contract to freeze is twofold. What the service exposes, as with the monolith's API. And what it consumes: the neighbours' responses and the message formats. A change on either side breaks silently.&lt;/p&gt;

&lt;p&gt;And here, you do have reviewers: the neighbouring teams. They do not read your Go, but they review your contract, and that is the one that matters. Formalize it with &lt;a href="https://docs.pact.io/" rel="noopener noreferrer"&gt;consumer-driven contract tests&lt;/a&gt;. Each neighbour writes what it expects from you, your CI checks it on every push.&lt;/p&gt;

&lt;p&gt;The release becomes the real review. Deploy the new version next to the old one, on a share of the traffic, and compare outputs before switching. In microservices, rolling back code is easy. Rolling back a message already published in a new format is not. Your rule: the service reads the old format and the new one for the whole transition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation is a deliverable, not a bonus
&lt;/h2&gt;

&lt;p&gt;On the last day of the assignment, the code will be orphaned again. So I prepare the exit from the entrance. An operations guide: how it runs, where the logs are, what to do when an import fails at three in the morning. A development guide: how to run the tests, update a golden file, deploy. And the list of decisions.&lt;/p&gt;

&lt;p&gt;The exit test is simple. A developer who does not know the project must be able to ship a small fix in one day, with these documents and the CI. If not, the assignment is not finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Capture the behaviour, freeze the contract, let CI say no.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Day 1: the system map before any change (inputs, outputs, batches, consumers)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;go vet&lt;/code&gt;, &lt;code&gt;staticcheck&lt;/code&gt;, &lt;code&gt;govulncheck&lt;/code&gt; as a first read&lt;/li&gt;
&lt;li&gt;[ ] Golden tests for every import tool, on real anonymized data&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;testscript&lt;/code&gt; for command-line tools&lt;/li&gt;
&lt;li&gt;[ ] One test per API route that records the current response&lt;/li&gt;
&lt;li&gt;[ ] An OpenAPI contract derived from the real code, validated by the consumer&lt;/li&gt;
&lt;li&gt;[ ] CI in place before the first feature, &lt;code&gt;go mod tidy -diff&lt;/code&gt; included&lt;/li&gt;
&lt;li&gt;[ ] A paragraph readable without Go at the top of every PR&lt;/li&gt;
&lt;li&gt;[ ] One ADR per structural decision&lt;/li&gt;
&lt;li&gt;[ ] No rewrite, no cleanup without a need or a red test&lt;/li&gt;
&lt;li&gt;[ ] The rollback written before the first deployment&lt;/li&gt;
&lt;li&gt;[ ] Microservice: the map comes from production traces, not from &lt;code&gt;go list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Microservice: contract frozen on both sides, exposed and consumed, checked by contract tests&lt;/li&gt;
&lt;li&gt;[ ] Microservice: new version in parallel on a share of the traffic, both formats read during the transition&lt;/li&gt;
&lt;li&gt;[ ] Operations and development guides delivered, tested by someone who does not know the project&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Without a reviewer, discipline replaces the second pair of eyes. Capture the behaviour, freeze the contract, let CI say no, write for the person who does not read the code.&lt;/p&gt;

&lt;p&gt;It is not slower. It is what lets you move fast without breaking something nobody will know how to fix.&lt;/p&gt;

&lt;p&gt;Got an orphaned Go island in your company? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; Michael Feathers, &lt;a href="https://www.informit.com/store/working-effectively-with-legacy-code-9780131177055" rel="noopener noreferrer"&gt;"Working Effectively with Legacy Code"&lt;/a&gt; (2004) · &lt;a href="https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript" rel="noopener noreferrer"&gt;testscript documentation&lt;/a&gt; · &lt;a href="https://pkg.go.dev/github.com/google/go-cmp/cmp" rel="noopener noreferrer"&gt;go-cmp documentation&lt;/a&gt; · &lt;a href="https://pkg.go.dev/net/http/httptest" rel="noopener noreferrer"&gt;net/http/httptest&lt;/a&gt; · &lt;a href="https://go.dev/doc/go1.23#go-command" rel="noopener noreferrer"&gt;Go 1.23, go mod tidy -diff&lt;/a&gt; · &lt;a href="https://adr.github.io/" rel="noopener noreferrer"&gt;Architecture Decision Records&lt;/a&gt; · method applied on a current assignment, client details deliberately left out&lt;/p&gt;

</description>
      <category>go</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>AI Act: Your Assistant Must Say It Is an AI. Since August 2, Not December 2</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/ai-act-your-assistant-must-say-it-is-an-ai-since-august-2-not-december-2-43a9</link>
      <guid>https://dev.to/julesrobineau/ai-act-your-assistant-must-say-it-is-an-ai-since-august-2-not-december-2-43a9</guid>
      <description>&lt;p&gt;On 2 December 2026, an AI Act deadline falls. Many teams remember that date and assume they have until then. For the essential part, that is wrong. The duty to tell a human they are talking to an AI has applied since 2 August. December 2 only covers one technical point, and only for systems already in service.&lt;/p&gt;

&lt;p&gt;I am not a lawyer. I run LLM agents in production, and one of them writes to strangers in my name. So I read Article 50 with a single question: what do I change in the code?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Article 50 of the AI Act has applied since 2 August 2026. A chatbot, an assistant, an agent that writes to people must say it is an AI. At the latest at the first exchange, unless it is obvious. Generated content must carry a machine-readable mark, with a delay until 2 December 2026 for systems already on the market. If you build on a third-party model API, you are the provider of your system: the obligation sits with you. Possible fine: up to 15 million euros or 3% of worldwide turnover. Here is the code-side runbook, in the format of &lt;a href="https://jrobineau.com/blog/2026-09-07-cra-reporting-runbook.html" rel="noopener noreferrer"&gt;the CRA one&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers and small teams with an AI assistant facing users, or an agent that sends messages. Dates and scope come from the primary sources listed at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Article 50 asks, in four lines
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://artificialintelligenceact.eu/" rel="noopener noreferrer"&gt;AI Act&lt;/a&gt; is the EU regulation on artificial intelligence. Most of its heavy obligations target high-risk systems, and those were pushed back to late 2027, even 2028. Article 50 targets everyone, and it is in force.&lt;/p&gt;

&lt;p&gt;It holds four obligations. One: a system designed to interact with people must inform them they are talking to an AI. Two: a system that generates text, images, audio or video must mark its outputs in a machine-readable format. Three: emotion recognition and biometric categorisation must be announced. Four: whoever spreads a &lt;a href="https://en.wikipedia.org/wiki/Deepfake" rel="noopener noreferrer"&gt;deepfake&lt;/a&gt;, or publishes generated text to inform the public on a matter of public interest, must say so.&lt;/p&gt;

&lt;p&gt;The first two concern you the moment an LLM faces a human. That is the subject of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dates, without confusion
&lt;/h2&gt;

&lt;p&gt;Article 50 applies since 2 August 2026. Full stop. The duty to inform users has no grace period.&lt;/p&gt;

&lt;p&gt;December 2, 2026 comes from the Digital Omnibus, a text that entered into force in late July 2026 and reshuffled the AI Act calendar. It grants a delay for one thing only: machine-readable marking, and only for systems placed on the market before 2 August. A system launched after 2 August must mark from day one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;August&lt;/span&gt; &lt;span class="m"&gt;2026&lt;/span&gt;     &lt;span class="n"&gt;inform&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;art&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="m"&gt;50.1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;announce&lt;/span&gt; &lt;span class="n"&gt;emotion&lt;/span&gt; &lt;span class="n"&gt;recognition&lt;/span&gt;
                  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="n"&gt;deepfakes&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;public&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;interest&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
                  &lt;span class="n"&gt;Everyone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;August&lt;/span&gt; &lt;span class="m"&gt;2026&lt;/span&gt;     &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;readable&lt;/span&gt; &lt;span class="n"&gt;marking&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="n"&gt;launched&lt;/span&gt;
                  &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;December&lt;/span&gt; &lt;span class="m"&gt;2026&lt;/span&gt;   &lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;readable&lt;/span&gt; &lt;span class="n"&gt;marking&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;systems&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;
                  &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;August&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;Penalty&lt;/span&gt;           &lt;span class="n"&gt;up&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;EUR&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;worldwide&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;art&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="m"&gt;99.4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the first line again. If your assistant does not introduce itself as an AI today, you are not early. You are late.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider or deployer? If you code the chatbot, it is you
&lt;/h2&gt;

&lt;p&gt;The AI Act separates two roles. The provider develops a system and places it on the market under their name. The deployer uses it under their responsibility. The duty to inform users falls on the provider. So does marking.&lt;/p&gt;

&lt;p&gt;The classic trap: "we use the OpenAI or Mistral API, so it is their problem". No. They are providers of their model. You develop a system from that model and put it under your name in front of your users. You are the provider of that system. The banner, the message, the marking: that is your code.&lt;/p&gt;

&lt;p&gt;You are also a deployer of the upstream model, but that takes nothing away. The two hats add up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 1: say it is an AI, at the first exchange
&lt;/h2&gt;

&lt;p&gt;The text requires the person to be informed "at the latest at the time of the first interaction", in a clear and distinguishable manner. Unless it is obvious to a reasonably well-informed and observant person, given the context.&lt;/p&gt;

&lt;p&gt;The Commission's guidelines give examples. Obvious: a coding assistant for professional developers, a non-player character in a video game. Not obvious: a help chatbot embedded in a website, an avatar, a companion. The exception is read narrowly. When in doubt, you inform.&lt;/p&gt;

&lt;p&gt;In code, that means four things. A message at the first exchange, per user, not once per deployment. In every language you serve. Accessible: readable by a screen reader, not a light grey line at the bottom. And a trace: the date this user saw the message. The day you are asked to prove it, a log beats a screenshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// First-interaction disclosure: per user, per language, logged.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AIDisclosureSeenAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RoleSystem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ai_disclosure"&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ai_disclosure_shown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkDisclosureSeen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The button's name matters too. In an app I built, the assistant is called "AI Assistant" in the menu, and the terms of use say so. That is already a good part of the job. An assistant called "Lea" with a photo, no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case that made me reread the text: my email agent
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://jrobineau.com/blog/2026-09-28-ai-agent-auto-reply-approval.html" rel="noopener noreferrer"&gt;My email agent&lt;/a&gt; reads my inbox, qualifies assignments and drafts the replies. In hybrid mode, it sends simple questions on its own, to recruiters who write to me. Signed with my name. Without a line saying a machine wrote it.&lt;/p&gt;

&lt;p&gt;The guidelines cover agentic systems. A system designed to interact with people is in scope, including when it acts on someone's behalf and addresses other people. And when the provider cannot know whether a human is on the other end, it informs at every interaction. A recruiter receiving my reply is a human. They cannot guess the agent wrote it. It is not obvious, so it has to be said.&lt;/p&gt;

&lt;p&gt;My reading, with a non-lawyer's caution. In review mode, I reread and send myself. The email is mine, the agent is a drafting tool. In hybrid mode, the agent sends without me. It is the one interacting. The line goes there, in code, not in the prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Disclosure only when the agent sends by itself.&lt;/span&gt;
&lt;span class="c"&gt;// A draft I review and send myself is my own message.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AutoSend&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AIDisclosure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// EN: "Reply drafted and sent by my automated assistant.&lt;/span&gt;
    &lt;span class="c"&gt;//      I read the whole thread and take over from the next step."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does it change the tone? A little. But a recruiter would rather know. And a system that has to hide to work has a bigger problem than the AI Act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obligation 2: mark generated content, "as far as technically feasible"
&lt;/h2&gt;

&lt;p&gt;The text requires machine-readable, detectable marking, "effective, interoperable, robust and reliable as far as technically feasible". For images, audio and video, techniques exist: signed metadata such as &lt;a href="https://c2pa.org/" rel="noopener noreferrer"&gt;C2PA&lt;/a&gt;, invisible watermarks. For text, it is weaker, and the Commission admits it: no single technique is enough, they must be stacked.&lt;/p&gt;

&lt;p&gt;Two important clarifications from the guidelines. Feasibility is assessed objectively: a small team's lack of resources is no excuse. And some outputs are out of scope, such as technical outputs between professionals. The text of a consumer chatbot is squarely in.&lt;/p&gt;

&lt;p&gt;The code of practice published in June 2026 is the marked path. It is voluntary, but if you do not sign it, you must demonstrate equivalent compliance by other means. In other words, you follow it anyway, without the trust bonus.&lt;/p&gt;

&lt;p&gt;What I do for text, for lack of better. Metadata wherever the format accepts it: an attribute on the rendered HTML block, a header on API responses, a field in exports. A log of generated outputs, with a hash of each text. So you can answer "yes, this text came from us" if someone shows it to you. And if the upstream model offers a text watermark, you turn it on, you do not strip it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Machine-readable marking on generated text, wherever the format allows.&lt;/span&gt;
&lt;span class="c"&gt;// No standard header exists yet: document yours in the compliance file.&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-AI-Generated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-AI-Generated-By"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"assistant/1.4; model="&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`&amp;lt;div class="answer" data-ai-generated="true"&amp;gt;%s&amp;lt;/div&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum256&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"generated_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not perfect. A copy-paste wipes everything. But it is feasible, documented, and dated. That is exactly what the text asks for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runbook I install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;BEFORE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt; &lt;span class="n"&gt;week&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;inventory&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;every&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt; &lt;span class="n"&gt;talks&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;human&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;deployer&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;is&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s"&gt;"you are talking to an AI"&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;explicit&lt;/span&gt; &lt;span class="n"&gt;assistant&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;terms&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="n"&gt;aligned&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;agents&lt;/span&gt; &lt;span class="n"&gt;that&lt;/span&gt; &lt;span class="n"&gt;send&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;their&lt;/span&gt; &lt;span class="n"&gt;own&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;disclosure&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;marking&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;where&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="n"&gt;allows&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt; &lt;span class="n"&gt;hashes&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="n"&gt;down&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;August&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;

&lt;span class="n"&gt;FILE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="n"&gt;December&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="n"&gt;systems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="n"&gt;describing&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;marking&lt;/span&gt; &lt;span class="n"&gt;measures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="n"&gt;they&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="n"&gt;feasible&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;exportable&lt;/span&gt; &lt;span class="n"&gt;disclosure&lt;/span&gt; &lt;span class="n"&gt;logs&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;practice&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;signed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;or&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;gap&lt;/span&gt; &lt;span class="n"&gt;documented&lt;/span&gt;

&lt;span class="n"&gt;THEN&lt;/span&gt;
&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;drill&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="n"&gt;asks&lt;/span&gt; &lt;span class="s"&gt;"was that an AI?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;you&lt;/span&gt; &lt;span class="n"&gt;prove&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ten&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing exotic. A message, a log, a description page. The difficulty is not technical, it is doing it before someone asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who enforces, in France
&lt;/h2&gt;

&lt;p&gt;Fines are national. In France, the scheme announced in September 2025 gives coordination and the single point of contact to the &lt;a href="https://fr.wikipedia.org/wiki/Direction_g%C3%A9n%C3%A9rale_de_la_Concurrence,_de_la_Consommation_et_de_la_R%C3%A9pression_des_fraudes" rel="noopener noreferrer"&gt;DGCCRF&lt;/a&gt;. The CNIL, the data protection authority, keeps biometrics and personal data. Arcom, the media regulator, takes deepfakes and online content. The ACPR, finance. On 2 August, the law designating these authorities had not yet been definitively adopted. The obligations apply anyway: the regulation is directly applicable.&lt;/p&gt;

&lt;p&gt;Do not count on that limbo. A complaint from a user or a competitor can arrive before the decree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A machine does not pass itself off as a person. And it leaves a trace of what it produces.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] For each system, you know whether you are provider or deployer, and you wrote it down&lt;/li&gt;
&lt;li&gt;[ ] Your assistant says it is an AI at the first exchange, per user, in every language&lt;/li&gt;
&lt;li&gt;[ ] The message is accessible and distinguishable, not grey text in a footer&lt;/li&gt;
&lt;li&gt;[ ] Your agent that sends messages on its own flags them as such, in code&lt;/li&gt;
&lt;li&gt;[ ] The assistant's name and description do not make it pass for a human&lt;/li&gt;
&lt;li&gt;[ ] Every display of the disclosure leaves a timestamped trace&lt;/li&gt;
&lt;li&gt;[ ] Generated text carries a machine-readable mark wherever the format allows&lt;/li&gt;
&lt;li&gt;[ ] A log keeps a hash of every generated output, with the model and the date&lt;/li&gt;
&lt;li&gt;[ ] The market date is written down: before 2 August, you have until 2 December for marking&lt;/li&gt;
&lt;li&gt;[ ] The code of practice is read: signed, or the gap documented&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Article 50 does not ask for technical magic. It asks you to stop passing a machine off as a person, and to keep a trace of what it produces.&lt;/p&gt;

&lt;p&gt;Most measures take a day to code. The date, though, has already passed.&lt;/p&gt;

&lt;p&gt;Got an LLM assistant or agent in production, and want it clean before someone asks? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng" rel="noopener noreferrer"&gt;Regulation (EU) 2024/1689, Articles 50 and 99 (EUR-Lex)&lt;/a&gt; · &lt;a href="https://artificialintelligenceact.eu/article/50/" rel="noopener noreferrer"&gt;Article 50 on artificialintelligenceact.eu&lt;/a&gt; · &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/guidelines-transparency-ai-generated-content" rel="noopener noreferrer"&gt;European Commission, guidelines on transparency obligations (Article 50)&lt;/a&gt; · &lt;a href="https://digital-strategy.ec.europa.eu/en/policies/code-practice-ai-generated-content" rel="noopener noreferrer"&gt;Code of practice on marking and labelling AI-generated content (final version, 10 June 2026)&lt;/a&gt; · &lt;a href="https://www.lewissilkin.com/insights/2026/07/27/the-digital-omnibus-on-ai-enters-into-force-today-102nedo" rel="noopener noreferrer"&gt;Lewis Silkin, the Digital Omnibus enters into force (27 July 2026)&lt;/a&gt; · &lt;a href="https://www.globalpolicywatch.com/2026/05/10-takeaways-european-commission-draft-guidelines-on-ai-transparency-under-the-eu-ai-act/" rel="noopener noreferrer"&gt;Covington, ten takeaways from the guidelines (May 2026)&lt;/a&gt; · &lt;a href="https://www.economie.gouv.fr/dgccrf/actualites-dgccrf/ia-la-dgccrf-coordonnera-laction-des-autorites-de-surveillance-en-france" rel="noopener noreferrer"&gt;economie.gouv.fr, DGCCRF to coordinate market surveillance authorities&lt;/a&gt; · &lt;a href="https://ai-regulation.com/eu-ai-act-implementation-france-still-without-designated-national-competent-authorities/" rel="noopener noreferrer"&gt;MIAI, France without designated authorities on 2 August 2026&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>compliance</category>
      <category>eu</category>
      <category>chatbots</category>
    </item>
    <item>
      <title>Why the npm Worm Does Not Work in Go, and Where Go Is Still Exposed</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 02 Oct 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/why-the-npm-worm-does-not-work-in-go-and-where-go-is-still-exposed-54b5</link>
      <guid>https://dev.to/julesrobineau/why-the-npm-worm-does-not-work-in-go-and-where-go-is-still-exposed-54b5</guid>
      <description>&lt;p&gt;On August 4, a worm crawled through npm. It started from a widely used cache library, &lt;code&gt;keyv&lt;/code&gt;, and spread from package to package on its own. Hundreds of poisoned packages in a few hours.&lt;/p&gt;

&lt;p&gt;Since then, I keep hearing the same question: "would that work in Go?" Short answer: that worm, no. Honest answer: Go has doors of its own, and some are wide open.&lt;/p&gt;

&lt;p&gt;I replayed every mechanism with Go 1.27 to check what I am saying. The outputs below are real.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: a worm needs three things. Run at install time, find a token to publish with, and spread without a human touching anything. npm offered all three. Go refuses all three by design. Nothing runs on download. There is no registry to publish to. And nothing moves without a change to &lt;code&gt;go.mod&lt;/code&gt;. But Go runs dependency code the moment you type &lt;code&gt;go test&lt;/code&gt;. It keeps a poisoned version in cache forever. And your update bot can open the door for it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for Go teams who read the npm headlines and want to know what applies to them. No knowledge of Go module internals needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the worm did, in three links
&lt;/h2&gt;

&lt;p&gt;On the morning of August 4, poisoned versions of &lt;code&gt;keyv&lt;/code&gt; and the &lt;code&gt;cacheable&lt;/code&gt; family appear on npm. A single line in &lt;code&gt;package.json&lt;/code&gt; does all the work: a &lt;code&gt;preinstall&lt;/code&gt; script. npm runs it automatically during &lt;code&gt;npm install&lt;/code&gt;, before the package code is even used.&lt;/p&gt;

&lt;p&gt;That script downloads a runtime and launches an obfuscated payload. It searches the machine: cloud keys, GitHub tokens, npm tokens, CI secrets. Then it uses the stolen npm tokens to republish poisoned versions of its victims' packages. And so on.&lt;/p&gt;

&lt;p&gt;The result: hundreds of packages hit, across organizations with no link to each other, in less than a morning. Socket flagged the first poisoned version six minutes after publication. Too late for the first &lt;code&gt;npm install&lt;/code&gt; runs.&lt;/p&gt;

&lt;p&gt;The detail that hurt: the poisoned versions carried valid &lt;a href="https://docs.npmjs.com/generating-provenance-statements" rel="noopener noreferrer"&gt;provenance&lt;/a&gt;, signed by GitHub Actions. The attacker did not break the signature. They pushed their code into the repository, and the legitimate release pipeline signed the poison. Chainguard put it well: provenance proves who published, not that the publisher's environment was trustworthy.&lt;/p&gt;

&lt;p&gt;Keep the mechanics in mind. Execution at install. A publishing token within reach. Automatic propagation to every project depending on the package. Three links. Let's look at Go link by link.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 1: in Go, downloading runs nothing
&lt;/h2&gt;

&lt;p&gt;It is a design goal the Go team wrote down in plain words. Neither fetching nor building a module may execute its code, even malicious code. There is no equivalent of &lt;code&gt;preinstall&lt;/code&gt;. A Go module is an archive of sources and a hash. Nothing else.&lt;/p&gt;

&lt;p&gt;I checked with a homemade poisoned dependency: a local module whose &lt;code&gt;init()&lt;/code&gt; function writes to stderr the moment it runs. Here is what the commands give:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;downloading&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;added&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;

&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="o"&gt;./...&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;vet&lt;/span&gt; &lt;span class="o"&gt;./...&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nothing&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;poisoned&lt;/span&gt; &lt;span class="n"&gt;dependency&lt;/span&gt; &lt;span class="n"&gt;did&lt;/span&gt; &lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="o"&gt;./...&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;evil&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="n"&gt;ran&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HOME&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;jules&lt;/span&gt; &lt;span class="n"&gt;GITHUB_TOKEN&lt;/span&gt; &lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Download, compile, analyze: nothing runs. The worm's first link does not exist. But read the last line. As soon as you run &lt;code&gt;go test&lt;/code&gt; or &lt;code&gt;go run&lt;/code&gt;, the dependency's code executes. With your environment, your variables, your tokens. I will come back to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 2: there is no registry to publish to
&lt;/h2&gt;

&lt;p&gt;The npm worm spreads by publishing. It has a token, it pushes a version, thousands of projects install it. In Go, there is nothing to push. A Go module is a tag in a git repository. The public proxy, &lt;code&gt;proxy.golang.org&lt;/code&gt;, fetches the tag itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;mod&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;
&lt;span class="s"&gt;"Sum"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0="&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"Origin"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"VCS"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"URL"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"https://github.com/google/uuid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Hash"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"0f11ee6918f41a04c201eceeadf612a377bc7fbc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"Ref"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"refs/tags/v1.6.0"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To publish a poisoned version of a Go library, you need write access to its git repository. A stolen GitHub token can do it, true. But that is no longer a registry token sitting in a config file. It is write access to a repository, more visible, more often protected by a review or a signature.&lt;/p&gt;

&lt;p&gt;And every version is carved in stone. The proxy computes a hash and records it in &lt;a href="https://go.dev/ref/mod#checksum-database" rel="noopener noreferrer"&gt;sum.golang.org&lt;/a&gt;, a public log nobody can edit after the fact. Everyone receives the same bytes, forever. An attacker cannot serve a clean version to auditors and a poisoned one to everyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 3: nothing moves without touching go.mod
&lt;/h2&gt;

&lt;p&gt;This is the real worm killer. In npm, a dependency declared as &lt;code&gt;^6.0.0&lt;/code&gt; automatically accepts 6.0.1. The worm publishes a poisoned 6.0.1, and the next &lt;code&gt;npm install&lt;/code&gt; takes it. No human decided anything.&lt;/p&gt;

&lt;p&gt;Go does the opposite. It picks the lowest version that satisfies everything &lt;code&gt;go.mod&lt;/code&gt; declares, never the latest published. That is minimal version selection, &lt;a href="https://go.dev/ref/mod#minimal-version-selection" rel="noopener noreferrer"&gt;MVS&lt;/a&gt;. A newly published version is never taken on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.5.0&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;

&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="o"&gt;./...&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;grep&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mod&lt;/span&gt;
&lt;span class="n"&gt;require&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.5.0&lt;/span&gt;    &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt; &lt;span class="n"&gt;leaves&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="n"&gt;alone&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing a version takes an explicit command, &lt;code&gt;go get&lt;/code&gt;, and a commit that modifies &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;go.sum&lt;/code&gt;. A poisoned upstream version stays out of your build as long as nobody asked for it. The worm can publish all it wants. Without a commit on your side, it does not crawl.&lt;/p&gt;

&lt;p&gt;And if someone tries to swap the bytes of an already known version, the build stops dead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;verifying&lt;/span&gt; &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;checksum&lt;/span&gt; &lt;span class="n"&gt;mismatch&lt;/span&gt;
    &lt;span class="n"&gt;downloaded&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;hNrb57e&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;94&lt;/span&gt;&lt;span class="n"&gt;F0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;AAAAJDMOsjHA8n1jAhLSgzrAzy1Hgr&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;hNrb57e&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;94&lt;/span&gt;&lt;span class="n"&gt;F0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;

&lt;span class="n"&gt;SECURITY&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt;
&lt;span class="n"&gt;This&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;does&lt;/span&gt; &lt;span class="n"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;earlier&lt;/span&gt; &lt;span class="n"&gt;download&lt;/span&gt; &lt;span class="n"&gt;recorded&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three links, three refusals. That worm does not get through Go. Now, the doors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 1: go test runs everything, with your secrets
&lt;/h2&gt;

&lt;p&gt;Back to the &lt;code&gt;[evil] init ran&lt;/code&gt; line. The npm worm stole tokens at install time. In Go, it just waits a little longer: the first &lt;code&gt;go test&lt;/code&gt;, the first &lt;code&gt;go run&lt;/code&gt;. On your machine, or in your CI, with the same environment variables.&lt;/p&gt;

&lt;p&gt;A poisoned dependency does not need &lt;code&gt;preinstall&lt;/code&gt;. An &lt;code&gt;init()&lt;/code&gt; function is enough. It runs before your &lt;code&gt;main&lt;/code&gt;, before your tests, without you calling it. In spring 2025, three ordinary-looking Go modules did exactly that: an obfuscated script that, on Linux, fetched a payload and wiped the disk.&lt;/p&gt;

&lt;p&gt;So the difference with npm is not "the code does not run". It is "the code does not run until you have added it to your build". That delay gives you time to read the diff of a new dependency. Use it, or the protection is worth nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 2: your update bot can change go.mod for you
&lt;/h2&gt;

&lt;p&gt;MVS protects you as long as a human decides versions. Many teams have delegated that decision to a bot. Me included: Renovate opens dependency PRs every day, and minor updates merge on their own once CI is green.&lt;/p&gt;

&lt;p&gt;Look at what that does to link 3. The worm publishes a poisoned version in the morning. The bot opens the PR at noon. CI runs &lt;code&gt;go test&lt;/code&gt;, hence the poisoned code, with the job's secrets. Then it goes green, and the PR merges. You just rebuilt npm's automatic propagation by hand.&lt;/p&gt;

&lt;p&gt;The answer fits in one setting: a delay before taking a new version. GitHub made it the default in Dependabot in late July, a week before the worm: a three-day wait. Renovate has the same option, &lt;code&gt;minimumReleaseAge&lt;/code&gt;. The poisoned &lt;code&gt;keyv&lt;/code&gt; versions lived for a few hours. With three days of latency, your bot would never have seen them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// renovate.json&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;"minimumReleaseAge"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"3 days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"packageRules"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"matchUpdateTypes"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"major"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s"&gt;"automerge"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Door 3: the proxy never forgets, poison included
&lt;/h2&gt;

&lt;p&gt;Proxy immutability protects in one direction and traps in the other. In 2021, someone publishes &lt;code&gt;github.com/boltdb-go/bolt&lt;/code&gt;, a copy of the real &lt;code&gt;boltdb/bolt&lt;/code&gt; library with a backdoor. That is &lt;a href="https://en.wikipedia.org/wiki/Typosquatting" rel="noopener noreferrer"&gt;typosquatting&lt;/a&gt;. The Go proxy caches the version.&lt;/p&gt;

&lt;p&gt;Then the attacker rewrites the tag on GitHub to point at clean code. An auditor reading the repository sees nothing. The proxy, meanwhile, serves the cached, poisoned version to everyone. For more than three years, until it was discovered in early 2025.&lt;/p&gt;

&lt;p&gt;The rule that guarantees "the same bytes for everyone" also guarantees "the same poisoned bytes for everyone". The hash log proves consistency, not innocence. Exactly like npm provenance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Door 4: what runs anyway
&lt;/h2&gt;

&lt;p&gt;Four cases run code you have not read, and you need to know them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go generate&lt;/code&gt; runs the commands written in &lt;code&gt;//go:generate&lt;/code&gt; comments. Never on its own, only when you type the command. Verified: my &lt;code&gt;go build&lt;/code&gt; did not trigger it, my &lt;code&gt;go generate&lt;/code&gt; did.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tool&lt;/code&gt; directive in &lt;code&gt;go.mod&lt;/code&gt;, since Go 1.24, declares tools as dependencies. Same rule: &lt;code&gt;go build&lt;/code&gt; does not start them, &lt;code&gt;go tool name&lt;/code&gt; runs them. A poisoned tool therefore runs when you call it, with your rights.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pkg.go.dev/cmd/cgo" rel="noopener noreferrer"&gt;cgo&lt;/a&gt; compiles C during &lt;code&gt;go build&lt;/code&gt;. A C compiler should not execute what it compiles. Unless there is a bug. CVE-2025-61732 was one: a difference in how Go and the C compiler read comments allowed code to be smuggled in and executed at compile time. Fixed since. Keep your toolchain current, and be wary of a dependency that brings C for no reason.&lt;/p&gt;

&lt;p&gt;And the most dangerous command is the most ordinary one: &lt;code&gt;go run github.com/someone/tool@latest&lt;/code&gt;. It is the exact equivalent of &lt;code&gt;npx&lt;/code&gt;. Download and immediate execution, floating version, no &lt;code&gt;go.mod&lt;/code&gt; to hold you back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I change in my pipelines after August 4
&lt;/h2&gt;

&lt;p&gt;My &lt;a href="https://jrobineau.com/blog/2026-07-29-go-supply-chain-security.html" rel="noopener noreferrer"&gt;supply chain pipeline from July&lt;/a&gt; still stands. Pin, scan, inventory, sign. The August worm adds three reflexes.&lt;/p&gt;

&lt;p&gt;A delay on automatic updates. Three days, not zero. A minor update is not worth the risk of being the first to install it.&lt;/p&gt;

&lt;p&gt;Tests that run with as few secrets as possible. The test job does not need the deploy key. If it has it, the poisoned dependency has it too. A short-lived &lt;a href="https://en.wikipedia.org/wiki/OpenID_Connect" rel="noopener noreferrer"&gt;OIDC&lt;/a&gt; token, requested only by the job that deploys, leaks nothing useful.&lt;/p&gt;

&lt;p&gt;A look at behavior, not only at CVEs. &lt;code&gt;govulncheck&lt;/code&gt; only knows published vulnerabilities. &lt;a href="https://github.com/google/capslock" rel="noopener noreferrer"&gt;capslock&lt;/a&gt; lists what a Go package is capable of: network access, running commands, reading files. A cache library that opens an outbound connection has a question to answer.&lt;/p&gt;

&lt;p&gt;On my test bench, it points straight at the poisoned dependency's &lt;code&gt;init()&lt;/code&gt;, which reads the environment, with the exact call path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;$&lt;/span&gt; &lt;span class="n"&gt;capslock&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt; &lt;span class="o"&gt;./...&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;span class="n"&gt;Analyzed&lt;/span&gt; &lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;evil&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="m"&gt;.0.0&lt;/span&gt;
  &lt;span class="n"&gt;github&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="m"&gt;.6.0&lt;/span&gt;

&lt;span class="n"&gt;READ_SYSTEM_STATE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;references&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="n"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="n"&gt;transitive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;Example&lt;/span&gt; &lt;span class="n"&gt;callpath&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
  &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;
  &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;evil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;
  &lt;span class="n"&gt;evil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;11&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dependency that shows up under &lt;code&gt;NETWORK&lt;/code&gt; or &lt;code&gt;EXEC&lt;/code&gt; with no business reason, you read before you keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The worm does not crawl through Go on its own. Do not lend it a hand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] No floating version: &lt;code&gt;go.mod&lt;/code&gt; and &lt;code&gt;go.sum&lt;/code&gt; committed, builds in &lt;code&gt;-mod=readonly&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;GOTOOLCHAIN=local&lt;/code&gt; in CI, with a pinned toolchain&lt;/li&gt;
&lt;li&gt;[ ] A three-day delay before any automatic version bump (&lt;code&gt;minimumReleaseAge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Major updates wait for a human, and the diff gets read&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;go test&lt;/code&gt; in CI without deploy secrets: short OIDC token, only in the job that deploys&lt;/li&gt;
&lt;li&gt;[ ] Never &lt;code&gt;go run pkg@latest&lt;/code&gt; on a machine that holds secrets&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;go generate&lt;/code&gt; and &lt;code&gt;go tool&lt;/code&gt; only on pinned tools, reviewed once&lt;/li&gt;
&lt;li&gt;[ ] A dependency that brings C or a networked &lt;code&gt;init()&lt;/code&gt; has to justify itself (&lt;code&gt;capslock&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;go mod verify&lt;/code&gt; and &lt;code&gt;govulncheck&lt;/code&gt; in the pipeline, as before&lt;/li&gt;
&lt;li&gt;[ ] No blind trust in a signature: provenance and sumdb prove origin, not intent&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Go did not get lucky on August 4. It had an architecture. No execution on download, no registry, no version that moves without a commit. The npm worm stops at each of those doors.&lt;/p&gt;

&lt;p&gt;But Go runs your dependencies at the first &lt;code&gt;go test&lt;/code&gt;, keeps the poison in cache, and your update bot can sign in your place. The worm does not crawl on its own. Do not lend it a hand.&lt;/p&gt;

&lt;p&gt;Want a second pair of eyes on your Go project's supply chain? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://socket.dev/blog/popular-npm-packages-in-the-keyv-and-cacheable-namespaces-compromised-in-active-supply-chain" rel="noopener noreferrer"&gt;Socket, keyv/cacheable compromise analysis (4 August 2026)&lt;/a&gt; · &lt;a href="https://www.chainguard.dev/unchained/the-keyv-and-cacheable-npm-supply-chain-attack-inside-the-mini-shai-hulud-campaign" rel="noopener noreferrer"&gt;Chainguard, "Inside the Mini Shai-Hulud campaign"&lt;/a&gt; · &lt;a href="https://github.blog/security/supply-chain-security/disrupting-supply-chain-attacks-on-npm-and-github-actions/" rel="noopener noreferrer"&gt;GitHub, "Disrupting supply chain attacks on npm and GitHub Actions" (28 July 2026)&lt;/a&gt; · &lt;a href="https://go.dev/blog/supply-chain" rel="noopener noreferrer"&gt;Go blog, "How Go Mitigates Supply Chain Attacks"&lt;/a&gt; · &lt;a href="https://go.dev/ref/mod#authenticating" rel="noopener noreferrer"&gt;Go modules reference, authentication&lt;/a&gt; · &lt;a href="https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence" rel="noopener noreferrer"&gt;Socket, the boltdb-go/bolt backdoor (February 2025)&lt;/a&gt; · &lt;a href="https://thehackernews.com/2025/05/malicious-go-modules-deliver-disk.html" rel="noopener noreferrer"&gt;The Hacker News, disk-wiping Go modules (May 2025)&lt;/a&gt; · &lt;a href="https://github.com/golang/go/issues/76697" rel="noopener noreferrer"&gt;golang/go#76697, CVE-2025-61732&lt;/a&gt; · outputs captured with Go 1.27.0 on my own test bench&lt;/p&gt;

</description>
      <category>backend</category>
      <category>cybersecurity</category>
      <category>go</category>
      <category>security</category>
    </item>
    <item>
      <title>Go 1.27 Detects Goroutine Leaks. I Tested It on Mine</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/go-127-detects-goroutine-leaks-i-tested-it-on-mine-6c4</link>
      <guid>https://dev.to/julesrobineau/go-127-detects-goroutine-leaks-i-tested-it-on-mine-6c4</guid>
      <description>&lt;p&gt;My &lt;a href="https://en.wikipedia.org/wiki/Server-sent_events" rel="noopener noreferrer"&gt;SSE&lt;/a&gt; streams have already leaked in production. Goroutines blocked forever, piling up for days. I told those incidents in &lt;a href="https://jrobineau.com/blog/2026-08-24-go-timeouts-sse-http2.html" rel="noopener noreferrer"&gt;the article about timeouts and SSE&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Go 1.27 shipped in August with a new tool: a profile that lists leaked goroutines. Not the waiting ones. The ones that have no chance of ever running again.&lt;/p&gt;

&lt;p&gt;So I replayed my real production leaks, and pointed it at them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Go 1.27's &lt;code&gt;goroutineleak&lt;/code&gt; profile uses the garbage collector to prove a goroutine can never run again. Zero false positives. Out of my four replayed leaks, it catches three, with the exact line. It misses the fourth: a global hub keeping a reference to the channel. Yet that is the most common leak in a real SSE server. Use both profiles: this one for proven leaks, the classic one for the rest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for Go developers running streaming or workers in production. No &lt;a href="https://pkg.go.dev/runtime/pprof" rel="noopener noreferrer"&gt;pprof&lt;/a&gt; knowledge needed to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a goroutine leak is
&lt;/h2&gt;

&lt;p&gt;A goroutine is a lightweight execution thread managed by Go. Your server starts one per request, plus every one your code creates.&lt;/p&gt;

&lt;p&gt;A leak is a blocked goroutine that will never run again. It waits on a channel nobody will read. Or on a context nobody will cancel.&lt;/p&gt;

&lt;p&gt;It burns no CPU. But it keeps its stack in memory, and everything it references too. A few thousand leaks, and your memory swells slowly. No crash, no log.&lt;/p&gt;

&lt;p&gt;Until now, we hunted these with the classic &lt;code&gt;goroutine&lt;/code&gt; profile. It lists everyone: the legitimately blocked and the doomed alike. The sorting was manual.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Go 1.27 brings
&lt;/h2&gt;

&lt;p&gt;Go 1.27 adds a second profile, named &lt;code&gt;goroutineleak&lt;/code&gt;. It existed as an experiment in Go 1.26. It is now on everywhere, no flag.&lt;/p&gt;

&lt;p&gt;The idea comes from research work done at Uber. The garbage collector already knows which objects are still reachable. The profile uses that. If a goroutine is blocked on a channel that no active goroutine can reach, nobody can ever wake it up.&lt;/p&gt;

&lt;p&gt;This is not a guess. It is a proof. Every listed goroutine is doomed, mathematically.&lt;/p&gt;

&lt;p&gt;You access it like any other profile: the &lt;code&gt;/debug/pprof/goroutineleak&lt;/code&gt; endpoint, or &lt;code&gt;pprof.Lookup("goroutineleak")&lt;/code&gt; in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  My test bench: my production leaks, replayed
&lt;/h2&gt;

&lt;p&gt;I replayed four leaks in a small program, with Go 1.27.0. All four come from my real production life.&lt;/p&gt;

&lt;p&gt;One: the timed-out worker. A goroutine computes and sends its result into an unbuffered channel. The caller gives up first. Nobody will ever read that channel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// The timed-out worker: nobody will ever read result&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// unbuffered&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c"&gt;// blocked forever&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;
&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="c"&gt;// caller gives up, the goroutine leaks&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two: the SSE producer without &lt;code&gt;ctx.Done()&lt;/code&gt;. It pushes events into a channel. The client disconnects, the handler returns, the producer stays blocked on its send.&lt;/p&gt;

&lt;p&gt;Three: the forgotten cancel. A goroutine waits on &lt;code&gt;&amp;lt;-ctx.Done()&lt;/code&gt; of a context whose &lt;code&gt;cancel&lt;/code&gt; nobody holds anymore.&lt;/p&gt;

&lt;p&gt;Four: the hub keeping the reference. A global map of subscribers, like in every SSE server. The client leaves, its channel stays in the map, the producer stays blocked on it.&lt;/p&gt;

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

&lt;p&gt;Here is the real output, summarized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;goroutine&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
&lt;span class="n"&gt;goroutine&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="n"&gt;goroutineleak&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt; &lt;span class="m"&gt;1.27&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
&lt;span class="n"&gt;goroutineleak&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;workerWithTimeout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func1&lt;/span&gt;    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;18&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sseProducerNoCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func1&lt;/span&gt;     &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;34&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forgottenCancel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func1&lt;/span&gt;      &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;47&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic profile counts five blocked goroutines. The new one accuses only three: the worker, the SSE producer, the forgotten context. Each stack points at the exact blocking line.&lt;/p&gt;

&lt;p&gt;In verbose mode, the displayed state changes too: &lt;code&gt;chan send (leaked)&lt;/code&gt;. The word &lt;code&gt;leaked&lt;/code&gt; is the signature. Not "maybe stuck". Doomed.&lt;/p&gt;

&lt;p&gt;Three leaks, zero false positives, the code line right there. You can alert on this counter without fearing noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it misses, and why it is the most common leak
&lt;/h2&gt;

&lt;p&gt;My fourth leak does not show up in the profile. The global hub still references the gone client's channel. For the garbage collector, that channel is reachable. In theory, someone could still read from it, or clean the map.&lt;/p&gt;

&lt;p&gt;In practice, nobody ever will. That goroutine is as dead as the other three. But the detector cannot prove it, so it stays silent.&lt;/p&gt;

&lt;p&gt;And there is the problem. In a real SSE server, the leak almost always goes through the hub. It is the hub that holds the clients' channels. My production leak was exactly that one.&lt;/p&gt;

&lt;p&gt;The docs own this limit: the profile cannot see goroutines blocked on objects still referenced. Remember the split. It proves. You still clean your hub on disconnect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I use it in production
&lt;/h2&gt;

&lt;p&gt;Both profiles, in this order.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;goroutineleak&lt;/code&gt; first. Everything it lists is a real leak. Fix it, no debate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Capture&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;pprof&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="c"&gt;//localhost:6060/debug/pprof/goroutineleak&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;Or&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt;
&lt;span class="n"&gt;curl&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;localhost&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="m"&gt;6060&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pprof&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;goroutineleak&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;goroutine&lt;/code&gt; profile next, comparing two captures one hour apart. A stack growing from one capture to the next is your "reachable" leak: the hub, the map, the pool.&lt;/p&gt;

&lt;p&gt;And the code guardrails stay mandatory. Every channel send happens inside a &lt;code&gt;select&lt;/code&gt; with &lt;code&gt;ctx.Done()&lt;/code&gt;. Every subscriber removes itself from the hub on disconnect, with a &lt;code&gt;defer delete&lt;/code&gt;. Every worker result goes through a buffered channel of size 1.&lt;/p&gt;

&lt;p&gt;On Go 1.26, the profile already exists: build with &lt;code&gt;GOEXPERIMENT=goroutineleakprofile&lt;/code&gt;. On 1.27, nothing to enable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The detector proves. Hygiene prevents.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Expose &lt;code&gt;/debug/pprof/goroutineleak&lt;/code&gt; on an internal port, never public&lt;/li&gt;
&lt;li&gt;[ ] Alert as soon as the counter exceeds zero: every entry is a real leak&lt;/li&gt;
&lt;li&gt;[ ] Compare two spaced &lt;code&gt;goroutine&lt;/code&gt; profiles for the leaks the detector cannot see&lt;/li&gt;
&lt;li&gt;[ ] Send on a channel only inside a &lt;code&gt;select&lt;/code&gt; with &lt;code&gt;ctx.Done()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Clean your hub on disconnect: &lt;code&gt;defer delete&lt;/code&gt; on the map&lt;/li&gt;
&lt;li&gt;[ ] Buffer a throwaway worker's result channel at size 1&lt;/li&gt;
&lt;li&gt;[ ] On Go 1.26, build with &lt;code&gt;GOEXPERIMENT=goroutineleakprofile&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Replay your past leaks in a small program: the profile output becomes your reference&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;Go 1.27 gives you a leak detector with zero false positives. That is rare, and precious. But it only proves what the garbage collector can prove. The most common SSE server leak, the hub keeping a reference, is still your job.&lt;/p&gt;

&lt;p&gt;Tools improve. Channel hygiene stays.&lt;/p&gt;

&lt;p&gt;Got a Go service whose memory grows for no visible reason? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://go.dev/doc/go1.27" rel="noopener noreferrer"&gt;Go 1.27 release notes&lt;/a&gt; · &lt;a href="https://github.com/golang/go/issues/74609" rel="noopener noreferrer"&gt;Proposal golang/go#74609, "new goroutine leak profile"&lt;/a&gt; · &lt;a href="https://antonz.org/detecting-goroutine-leaks/" rel="noopener noreferrer"&gt;Anton Zhiyanov, "Detecting goroutine leaks"&lt;/a&gt; · profile outputs captured with Go 1.27.0 on my own repros&lt;/p&gt;

</description>
      <category>go</category>
      <category>debugging</category>
      <category>performance</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your AI Assistant Can Reply to a Stranger. Do You Let It?</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/your-ai-assistant-can-reply-to-a-stranger-do-you-let-it-59f3</link>
      <guid>https://dev.to/julesrobineau/your-ai-assistant-can-reply-to-a-stranger-do-you-let-it-59f3</guid>
      <description>&lt;p&gt;For months, an agent has been reading my inbox. An agent is a program driven by a language model, and it can act: read, sort, write. Mine handles mission proposals. It sorts them, evaluates them, and drafts the replies.&lt;/p&gt;

&lt;p&gt;One question remains: who presses send?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my agent reads everything and drafts everything, without asking me. But for sending, each type of reply has its own rule. A simple question goes out alone, twice at most, with a delay. A refusal always waits for my click. A serious mission always comes back to me. And these rules live in code: no prompt can change them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for developers who want to wire an agent into their inbox without handing it their signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenClaw made the question concrete
&lt;/h2&gt;

&lt;p&gt;OpenClaw is the open source AI assistant that blew up this year. It runs on your machine. It reads your messages. And it can answer them for you.&lt;/p&gt;

&lt;p&gt;Security did not keep up. Censys counted 21,639 instances open on the internet. The extension marketplace hosted over a thousand malicious packages. Krebs on Security ran an investigation.&lt;/p&gt;

&lt;p&gt;Those flaws get fixed with patches. But one question survives, even fully patched: do you let a machine talk to a stranger in your name?&lt;/p&gt;

&lt;p&gt;I live with that question. My agent runs in production, on my real inbox. Here are the rules I gave it, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real risk is what leaves
&lt;/h2&gt;

&lt;p&gt;When the agent reads, I risk my data. That is serious, but manageable: it runs locally, with minimal access.&lt;/p&gt;

&lt;p&gt;When the agent writes to a stranger, I risk my name. A bad draft takes two seconds to fix. A bad email, sent to a prospect, cannot be erased.&lt;/p&gt;

&lt;p&gt;So my rule is not about what the agent can do. It is about what leaves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent does on its own
&lt;/h2&gt;

&lt;p&gt;All the reading and all the preparation run without me.&lt;/p&gt;

&lt;p&gt;First, a dumb filter. The email must contain at least two words from the mission world. No AI here: a word list, fast and predictable. Newsletters stop there.&lt;/p&gt;

&lt;p&gt;Then a local model reads the email. It fills a five-criteria grid: stack, contract type, start date, rate, remote policy. Each criterion gets a verdict: &lt;code&gt;GO&lt;/code&gt;, &lt;code&gt;NO_GO&lt;/code&gt; or unknown.&lt;/p&gt;

&lt;p&gt;Finally, a cloud model drafts a reply in my style. The result: a draft. Nothing else.&lt;/p&gt;

&lt;p&gt;I review none of these steps. If the agent gets something wrong here, it costs nothing. Nothing has left.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it sends alone, and what it never will
&lt;/h2&gt;

&lt;p&gt;When it is time to reply, the agent picks an action. Each action has its own sending rule. It is a Go &lt;code&gt;switch&lt;/code&gt;, not a model mood.&lt;/p&gt;

&lt;p&gt;The rate or remote policy is missing? The agent asks, and sends it alone. A simple question commits nothing.&lt;/p&gt;

&lt;p&gt;Declining a mission? Never alone. A refusal closes a door, with my name at the bottom. The draft waits for my click. In the code, auto-send is hardcoded off for that action.&lt;/p&gt;

&lt;p&gt;Sending my skills file? Never alone either.&lt;/p&gt;

&lt;p&gt;And when a mission checks at least three criteria out of five, with zero &lt;code&gt;NO_GO&lt;/code&gt;? The agent stops replying. It summarizes the thread and hands it back to me.&lt;/p&gt;

&lt;p&gt;Look at the logic. The more serious the conversation gets, the fewer rights the agent has. Never the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two guardrails, even on simple questions
&lt;/h2&gt;

&lt;p&gt;Even the automatic sending of questions keeps two limits.&lt;/p&gt;

&lt;p&gt;A cap: two automatic sends per conversation. It is a counter in code, not a promise in a prompt. On the third exchange, everything becomes a draft again. A loop with a stranger must stop on its own.&lt;/p&gt;

&lt;p&gt;A delay: each send waits between two and six hours, at random. The reply looks human. And while it waits, the draft sits visible in my mailbox. I delete it, the send fails. That delay is my veto right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules live in code, not in the prompt
&lt;/h2&gt;

&lt;p&gt;My agent starts in a mode, picked by a flag: &lt;code&gt;draft&lt;/code&gt;, &lt;code&gt;hybrid&lt;/code&gt; or &lt;code&gt;review&lt;/code&gt;. In &lt;code&gt;draft&lt;/code&gt;, nothing ever leaves. In &lt;code&gt;hybrid&lt;/code&gt;, only simple questions leave alone. In &lt;code&gt;review&lt;/code&gt;, everything waits for my approval.&lt;/p&gt;

&lt;p&gt;The prompt decides none of this. That is the most important point in this article.&lt;/p&gt;

&lt;p&gt;Why? Because of &lt;a href="https://en.wikipedia.org/wiki/Prompt_injection" rel="noopener noreferrer"&gt;prompt injection&lt;/a&gt;. A booby-trapped email can talk a model into changing its mind. It cannot change a compiled flag.&lt;/p&gt;

&lt;p&gt;I already tested this philosophy on permissions: &lt;a href="https://jrobineau.com/blog/2026-07-23-ai-agent-permissions-dev-vs-prod.html" rel="noopener noreferrer"&gt;my AI agent tried to delete my secrets, it could not&lt;/a&gt;. Sending follows the same rule. It is enforced outside the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  And when I want to approve everything: review mode
&lt;/h2&gt;

&lt;p&gt;Sometimes I want zero automatic sending. I switch to &lt;code&gt;review&lt;/code&gt; mode.&lt;/p&gt;

&lt;p&gt;Every reply then lands in a small web interface, at home, never exposed to the internet. Three buttons: approve, edit, reject. If I reject, I explain in one sentence, and the agent rewrites the draft with my feedback. Every decision goes to a log, for retraining later.&lt;/p&gt;

&lt;p&gt;For LinkedIn, I go even further. An extension pastes the reply into the message box. It never sends. The final click stays mine, physically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist for your agent
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;One rule per type of action, never per conversation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] List what your agent can do, action by action&lt;/li&gt;
&lt;li&gt;[ ] For each action, three questions: reversible? my name? a stranger?&lt;/li&gt;
&lt;li&gt;[ ] Let reading and preparation run without validation&lt;/li&gt;
&lt;li&gt;[ ] Allow solo sending only for simple questions, with a cap&lt;/li&gt;
&lt;li&gt;[ ] Add a delay before every auto-send: that is your veto right&lt;/li&gt;
&lt;li&gt;[ ] Put the rules in code, a flag or a switch, never in the prompt&lt;/li&gt;
&lt;li&gt;[ ] Keep a trace of every decision, to reread later&lt;/li&gt;
&lt;li&gt;[ ] Keep the approval interface local, away from the internet&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;So, do you let it? Reading, sorting, preparing: yes, fully. Talking to strangers: each action has its rule, with a cap, a delay, and a mode where everything goes through you.&lt;/p&gt;

&lt;p&gt;OpenClaw did not create this problem. It scaled it. Your agent's freedom is an architecture decision, not a prompt line.&lt;/p&gt;

&lt;p&gt;Want to wire an agent into your inbox without handing it your signature? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://krebsonsecurity.com/2026/03/how-ai-assistants-are-moving-the-security-goalposts/" rel="noopener noreferrer"&gt;Krebs on Security, "How AI Assistants Are Moving the Security Goalposts"&lt;/a&gt; · &lt;a href="https://www.reco.ai/blog/openclaw-the-ai-agent-security-crisis-unfolding-right-now" rel="noopener noreferrer"&gt;Reco, "OpenClaw: The AI Agent Security Crisis"&lt;/a&gt; · &lt;a href="https://www.paloaltonetworks.com/blog/identity-security/how-autonomous-ai-agents-like-openclaw-are-reshaping-enterprise-identity-security/" rel="noopener noreferrer"&gt;Palo Alto Networks, on autonomous agent identity&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>llm</category>
    </item>
    <item>
      <title>Taking Over an AI-Built App: The Audit Before the Quote</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 25 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/taking-over-an-ai-built-app-the-audit-before-the-quote-1oam</link>
      <guid>https://dev.to/julesrobineau/taking-over-an-ai-built-app-the-audit-before-the-quote-1oam</guid>
      <description>&lt;p&gt;A new kind of gig is landing in freelancers' inboxes. Non-technical founders, an app built entirely by AI, real users on the way. And one question: "can you finish the job?"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I just lived that gig. My rule: never quote on a phone call. One day of scoping audit, unpaid but time-boxed, before any number. AI-generated apps share documented vulnerability patterns, and a tight budget gets a quote split into lots, security first. The precise findings stay with the client. The method is worth sharing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for freelancers receiving these requests, and for founders about to send one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical 2026 meeting
&lt;/h2&gt;

&lt;p&gt;The scene is becoming standard. Two founders, no developer on the team. An app built on an AI app builder, Lovable or similar, until the credits ran out.&lt;/p&gt;

&lt;p&gt;Then a developer friend takes a look and raises the alarm: personal data exposed. The founders freeze all changes and look for a professional.&lt;/p&gt;

&lt;p&gt;Add the classic constraints. A budget of a few thousand euros. A store deadline in weeks, not months. Several freelancers competing.&lt;/p&gt;

&lt;p&gt;I just lived that meeting. It is about to become routine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I never quote on a call
&lt;/h2&gt;

&lt;p&gt;Without reading the code, a takeover estimate swings by a factor of ten. Mine melted after reading: the code was better than feared.&lt;/p&gt;

&lt;p&gt;The trap cuts both ways. Quote too high and you scare away a viable client. Quote too low and you lock yourself into a losing fixed price.&lt;/p&gt;

&lt;p&gt;My rule: a one-day scoping audit before any quote. Unpaid, because it is pre-sales against competitors. Time-boxed, because unlimited unpaid work is a slope.&lt;/p&gt;

&lt;p&gt;The deliverable is double: a report readable by non-technical people, and a split quote the report justifies line by line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I look at in one day
&lt;/h2&gt;

&lt;p&gt;One day does not buy exhaustiveness. It buys the patterns that decide the quote. My reading order never changes.&lt;/p&gt;

&lt;p&gt;First, the data access model. Who talks to the database, with which privileges? That question dominates all the others.&lt;/p&gt;

&lt;p&gt;Then the public endpoints: what they return, field by field. Then everything that touches money: types, validations, payment provider webhooks.&lt;/p&gt;

&lt;p&gt;Then the git history and the migrations. The real state of an AI-generated database often diverges from what the files claim.&lt;/p&gt;

&lt;p&gt;Finally, secrets and their hygiene. And the good parts, noted with the same seriousness: they make the rest of the report credible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The patterns you will find
&lt;/h2&gt;

&lt;p&gt;The flaws of AI-generated apps are not artisanal surprises. They are documented, and they repeat.&lt;/p&gt;

&lt;p&gt;The public reference: CVE-2025-48757. A researcher scanned apps built with Lovable. About 10% had Row Level Security missing or broken. RLS is the Postgres mechanism that filters rows per user.&lt;/p&gt;

&lt;p&gt;The sneakiest pattern is subtler: correct RLS policies, but a server that queries the database with the admin key. The barrier exists, everything steps over it. A bypassed policy protects nothing.&lt;/p&gt;

&lt;p&gt;The next ones show up almost every time. Public endpoints returning every field instead of the strict minimum. No rate limiting, so enumeration is trivial. Money handled as floating-point numbers, with no database constraints.&lt;/p&gt;

&lt;p&gt;My favorite: AI-generated security helpers, clean, tested, and never wired in. The tool writes the masking function, then never calls it.&lt;/p&gt;

&lt;p&gt;In the app I just audited, several of these patterns showed up. I will not say which ones: precise findings belong to the client. The patterns belong to everyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quote in lots, or nothing
&lt;/h2&gt;

&lt;p&gt;The announced budget rarely covers the dream scope. The gap is handled by splitting, not by haggling.&lt;/p&gt;

&lt;p&gt;Lot 1: data security. Close the leaks, control access, add the critical constraints. This lot fits the envelope, and it covers what can hurt users.&lt;/p&gt;

&lt;p&gt;Lot 2: reliability of the money flows. Lot 3: production readiness. Each has its price, and the client chooses with full knowledge.&lt;/p&gt;

&lt;p&gt;And one red line: never a global fixed price "until store approval". Review round-trips do not depend on me. Promising a deadline a third party controls is manufacturing a dispute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I tell the founders
&lt;/h2&gt;

&lt;p&gt;Three messages, straight.&lt;/p&gt;

&lt;p&gt;One: your generated app is not garbage. The structure was readable, tests existed, some reflexes were right. The report says so, because it is true and because it makes the warnings credible.&lt;/p&gt;

&lt;p&gt;Two: security comes before features, because real data and real payments are flowing. Lot 1 is not negotiable in content, only in schedule.&lt;/p&gt;

&lt;p&gt;Three: regulatory obligations do not vanish by switching providers. What is mandatory with one will be mandatory with the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoping audit checklist
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;One day, in this order. The quote almost writes itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Time-box the audit: one day, a written deliverable, not an hour more&lt;/li&gt;
&lt;li&gt;[ ] Start with the access model: who talks to the database, with which privileges&lt;/li&gt;
&lt;li&gt;[ ] Hunt for paths that bypass RLS with an admin key&lt;/li&gt;
&lt;li&gt;[ ] List public endpoints and every field they return&lt;/li&gt;
&lt;li&gt;[ ] Follow the money: types, database constraints, webhooks and their checks&lt;/li&gt;
&lt;li&gt;[ ] Compare the database's real state with what the migrations claim&lt;/li&gt;
&lt;li&gt;[ ] Check secrets in git, across the whole history&lt;/li&gt;
&lt;li&gt;[ ] Note the good parts, and split the quote into lots, security first&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;AI-built app takeovers are about to fill freelancers' inboxes. The differentiating skill is not despising that code. It is auditing it fast, and quoting honestly.&lt;/p&gt;

&lt;p&gt;One audit day, known patterns, a quote in lots. The rest is execution.&lt;/p&gt;

&lt;p&gt;Have a generated app to secure before launch, or a takeover to scope? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://mattpalmer.io/posts/2025/05/CVE-2025-48757/" rel="noopener noreferrer"&gt;Matt Palmer, CVE-2025-48757 (missing RLS in Lovable apps)&lt;/a&gt; · &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2025-48757" rel="noopener noreferrer"&gt;NVD, CVE-2025-48757 entry&lt;/a&gt; · &lt;a href="https://supabase.com/docs/guides/database/postgres/row-level-security" rel="noopener noreferrer"&gt;Supabase, Row Level Security documentation&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>freelance</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Retraining Silently Deleted a Class</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Wed, 23 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/my-retraining-silently-deleted-a-class-3g6</link>
      <guid>https://dev.to/julesrobineau/my-retraining-silently-deleted-a-class-3g6</guid>
      <description>&lt;p&gt;Nobody touched the code. Nobody changed the model. I just tidied my mailbox. And my classifier lost an entire class, without a single warning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my email triage bot classifies with a TF-IDF model trained on my own folders. A manual re-sort drained the Personnel class from 196 examples to 9. The next retraining pushed it under the minimum-class threshold and deployed a 6-class model instead of 7. Silently. Here is the incident, and the three guardrails I built so it can never happen again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for everyone who can retrain a model with one command, and has no alarm around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My email triage bot has been running for months. I told its story in &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;Put the LLM Last&lt;/a&gt;: rules first, a classifier next, the LLM as a last resort.&lt;/p&gt;

&lt;p&gt;The classifier is TF-IDF with logistic regression. Training happens in Python, inference in pure Go. Parity between both was verified: 5,895 emails out of 5,895, same verdict.&lt;/p&gt;

&lt;p&gt;Labels come from real usage: the folder each email ends up in. 5,895 labeled emails, 7 classes kept for training.&lt;/p&gt;

&lt;p&gt;And retraining takes one command: &lt;code&gt;make retrain&lt;/code&gt;. That convenience is what bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident: nobody touched the code
&lt;/h2&gt;

&lt;p&gt;One weekend, I cleaned up my mailbox. The emails in the Personnel folder moved to other, better-organized folders.&lt;/p&gt;

&lt;p&gt;On the data side: the Personnel class went from 196 examples to 9.&lt;/p&gt;

&lt;p&gt;The training pipeline has a healthy threshold: a class with too few examples is excluded from the model. Healthy, except the exclusion was silent.&lt;/p&gt;

&lt;p&gt;So the next &lt;code&gt;make retrain&lt;/code&gt; trained a 6-class model instead of 7. It got deployed. No message, no refusal, no visible difference.&lt;/p&gt;

&lt;p&gt;The symptom came later: personal emails filed anywhere. The pipeline did exactly what it was told. That is precisely the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it is structural: your labels are alive
&lt;/h2&gt;

&lt;p&gt;This bug is not a local blunder. It is structural, and it is waiting for you too.&lt;/p&gt;

&lt;p&gt;My labels are generated by a living system: my mailbox and my sorting habits. One re-sort, one archive, one new habit, and the class distribution shifts.&lt;/p&gt;

&lt;p&gt;Your dataset is not a file. It is a snapshot of something that moves.&lt;/p&gt;

&lt;p&gt;A retraining pipeline only sees today's snapshot. Without a memory of the previous model, it cannot know a class disappeared. You have to give it that memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 1: expected classes
&lt;/h2&gt;

&lt;p&gt;First guardrail, the simplest. The retraining script first reads the deployed model's classes. Then it requires finding them in the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;retrain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="n"&gt;reads&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;deployed&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;enforces&lt;/span&gt; &lt;span class="n"&gt;them&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="nb"&gt;new&lt;/span&gt; &lt;span class="n"&gt;training&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;
&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="n"&gt;Cabinet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Maison&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;Newsletter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;an&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="n"&gt;under&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;minimum&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="n"&gt;ABORT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Nothing&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nothing&lt;/span&gt; &lt;span class="n"&gt;deployed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A class can still disappear. But it is now an explicit human decision, not a side effect of tidying up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 2: a quality gate against the deployed model
&lt;/h2&gt;

&lt;p&gt;The first guardrail catches a vanishing class. It misses a class that survives but collapses.&lt;/p&gt;

&lt;p&gt;So every model now embeds its metrics in its own file: accuracy, macro-F1, per-class F1. Macro-F1 averages each class's F1: small classes weigh as much as big ones.&lt;/p&gt;

&lt;p&gt;On the next retraining, the script reads the deployed model's macro-F1 and takes it as the floor. The candidate regresses beyond the tolerated margin? Abort, before anything is written.&lt;/p&gt;

&lt;p&gt;The gate is tested like code: a simulated collapse must trigger the abort. An untested guardrail is a decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrail 3: the escalation monitor
&lt;/h2&gt;

&lt;p&gt;The first two guardrails protect training. The third one watches production.&lt;/p&gt;

&lt;p&gt;When the classifier is unsure, it escalates: below a confidence threshold, the decision goes to the fallback LLM. That escalation rate is the model's heartbeat.&lt;/p&gt;

&lt;p&gt;Every decision writes one journal line: timestamp, confidence, escalated or not, label. Never the subject or the sender, the journal is persistent.&lt;/p&gt;

&lt;p&gt;A small tool aggregates by week and alerts when the recent rate exceeds the baseline by more than 10 points. When the world changes, the model doubts more. Doubt is measurable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not do
&lt;/h2&gt;

&lt;p&gt;I did not rebuild the Personnel class. The data was scattered and polluted, and rebuilding would have cost more than the class was worth.&lt;/p&gt;

&lt;p&gt;Personnel went back to deterministic rules, with the LLM as a net. The 6-class model became the documented, accepted reference.&lt;/p&gt;

&lt;p&gt;Not every class deserves ML. A class poor in examples lives better as rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before your next retrain
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Three guardrails, one evening of work. The incident costs weeks of trust.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Make the script list the deployed model's classes before training&lt;/li&gt;
&lt;li&gt;[ ] Refuse the retrain if an expected class falls under the threshold&lt;/li&gt;
&lt;li&gt;[ ] Embed the metrics inside the deployed model's file&lt;/li&gt;
&lt;li&gt;[ ] Compare candidate to deployed: regression beyond the margin, abort&lt;/li&gt;
&lt;li&gt;[ ] Test your guardrails: a simulated collapse must trigger the abort&lt;/li&gt;
&lt;li&gt;[ ] Journal every production decision: confidence, escalation, no content&lt;/li&gt;
&lt;li&gt;[ ] Alert on the escalation rate, not only on errors&lt;/li&gt;
&lt;li&gt;[ ] Accept moving a class out of ML when rules do better&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;A retraining pipeline without memory destroys your model silently, because data moves without notice.&lt;/p&gt;

&lt;p&gt;The defense has three parts: expected classes, a quality floor read from the deployed model, and a watched escalation rate.&lt;/p&gt;

&lt;p&gt;None of this requires an MLOps platform. An honest script is enough.&lt;/p&gt;

&lt;p&gt;An ML pipeline to make trustworthy, even a small one? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://developers.google.com/machine-learning/guides/rules-of-ml" rel="noopener noreferrer"&gt;Google, Rules of Machine Learning (monitoring and degradation)&lt;/a&gt; · &lt;a href="https://scikit-learn.org/stable/modules/generated/sklearn.metrics.f1_score.html" rel="noopener noreferrer"&gt;scikit-learn, f1_score and macro averaging&lt;/a&gt; · &lt;a href="https://jrobineau.com/blog/put-the-llm-last.html" rel="noopener noreferrer"&gt;Put the LLM Last, the bot's architecture&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>go</category>
      <category>python</category>
    </item>
    <item>
      <title>ABAC in Production: What Actually Breaks</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Mon, 21 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/abac-in-production-what-actually-breaks-13mc</link>
      <guid>https://dev.to/julesrobineau/abac-in-production-what-actually-breaks-13mc</guid>
      <description>&lt;p&gt;RBAC versus ABAC comparisons fill entire pages of search results. Almost all of them stop right at the interesting part: production.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: I built an ABAC system for application-level permissions on a platform with more than 25 million users. In production, the policies are not what breaks. Stale attributes, rule explosion, missing enforcement points and silent decisions are. Four failure modes, and the rules that contain them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for teams outgrowing simple roles and preparing the jump to fine-grained authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Two definitions first. RBAC grants rights through roles: admin, editor, viewer. ABAC decides through attributes: who is asking, on which resource, in which context.&lt;/p&gt;

&lt;p&gt;On the healthcare platform I work on, application permissions depend on the user, the application and the site. More than 25 million users, more than 1,000 sites.&lt;/p&gt;

&lt;p&gt;With roles alone, that kind of matrix ends in explosion. One role per combination, hundreds of roles, nobody knows who can do what anymore. That is the classic road to ABAC.&lt;/p&gt;

&lt;p&gt;But ABAC does not remove complexity. It moves it: from roles to attributes and policies. And that is where production waits for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RBAC versus ABAC debate is over
&lt;/h2&gt;

&lt;p&gt;The model war ended a while ago. In practice everyone lands on hybrid: RBAC for the baseline, ABAC for the fine grain.&lt;/p&gt;

&lt;p&gt;The tooling matured too. OpenFGA became a CNCF incubating project in late 2025. SpiceDB powers the permissions behind ChatGPT Enterprise connectors, through AuthZed. Google's Zanzibar model has many children now.&lt;/p&gt;

&lt;p&gt;So picking a model is no longer the real problem. The real problem is what breaks afterwards. And almost nobody writes that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1: stale attributes
&lt;/h2&gt;

&lt;p&gt;An ABAC decision is only as fresh as its oldest attribute.&lt;/p&gt;

&lt;p&gt;Attributes come from elsewhere. A directory, a site registry, a contract. They synchronize, therefore they go stale.&lt;/p&gt;

&lt;p&gt;The trap is silent. A perfect policy, evaluated on wrong attributes, returns wrong decisions with perfect confidence.&lt;/p&gt;

&lt;p&gt;My rules. Every attribute has a designated source of truth. Every attribute has a maximum accepted age. And a missing attribute closes access, never the opposite.&lt;/p&gt;

&lt;p&gt;Fail closed, always. Fail closed means: when in doubt or during an outage, deny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 2: rule explosion
&lt;/h2&gt;

&lt;p&gt;Second failure mode: policies pile up.&lt;/p&gt;

&lt;p&gt;Every edge case becomes a rule. Nobody dares delete a rule they no longer understand. Two rules end up contradicting each other, and evaluation order becomes superstition.&lt;/p&gt;

&lt;p&gt;My rules. Every policy has a named owner. Policies get reviewed on a fixed schedule, and a policy without a living owner gets deleted.&lt;/p&gt;

&lt;p&gt;Above all: decision tests. Golden cases replayed on every change: this user, this resource, this context, this expected verdict. A policy without regression tests is a roulette wheel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 3: missing enforcement points
&lt;/h2&gt;

&lt;p&gt;The most dangerous failure mode is not a bad policy. It is a code path that never asks the question.&lt;/p&gt;

&lt;p&gt;A new endpoint. A batch job. An export. An admin tool. Each one can read data without asking for permission.&lt;/p&gt;

&lt;p&gt;A policy without an enforcement point is an opinion.&lt;/p&gt;

&lt;p&gt;My rules. One decision gate, a library or a service, and every path goes through it. A second barrier in the database for the most sensitive data.&lt;/p&gt;

&lt;p&gt;And tests that attack paths, not policies. Call the endpoint without the right. Expect a denial. Every route must know how to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 4: silent decisions
&lt;/h2&gt;

&lt;p&gt;Last mode: silence. An access denial with no trace creates two problems.&lt;/p&gt;

&lt;p&gt;Support cannot answer "why don't I have access?". And audit cannot answer "who accessed what". On a healthcare platform, the second question is not optional.&lt;/p&gt;

&lt;p&gt;My rules. Log every decision, granted or denied, with the attributes that were evaluated. Watch the journal's content: identifiers and categories, no plaintext personal data.&lt;/p&gt;

&lt;p&gt;And a replay tool for support. Same question, same attributes, the decision explained. "Why" should take a minute, not a day of archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist before you adopt ABAC
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The four failure modes do not depend on the tool you pick. Neither does this checklist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Stay hybrid: RBAC for the baseline, ABAC where fine grain pays&lt;/li&gt;
&lt;li&gt;[ ] Give every attribute a source of truth, a max age, and fail closed&lt;/li&gt;
&lt;li&gt;[ ] Name an owner per policy, delete orphaned policies&lt;/li&gt;
&lt;li&gt;[ ] Write golden decision cases, replayed on every change&lt;/li&gt;
&lt;li&gt;[ ] One decision gate, and every path goes through it&lt;/li&gt;
&lt;li&gt;[ ] Test the paths without the right: every endpoint must know how to deny&lt;/li&gt;
&lt;li&gt;[ ] Log every decision with its attributes, no plaintext PII&lt;/li&gt;
&lt;li&gt;[ ] Tool the "why": support must be able to replay a decision&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;ABAC keeps its promise: fine-grained rights that follow the business. But its reliability does not live in the policies. It lives in the attributes, the enforcement points and the logs.&lt;/p&gt;

&lt;p&gt;Starting from scratch today, look at OpenFGA or SpiceDB before writing your own. The four failure modes will follow you whatever the tool.&lt;/p&gt;

&lt;p&gt;An authorization system to design or to harden? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://csrc.nist.gov/pubs/sp/800/162/upd2/final" rel="noopener noreferrer"&gt;NIST SP 800-162, ABAC guide&lt;/a&gt; · &lt;a href="https://www.cncf.io/blog/2025/11/11/openfga-becomes-a-cncf-incubating-project/" rel="noopener noreferrer"&gt;CNCF, OpenFGA becomes an incubating project (November 2025)&lt;/a&gt; · &lt;a href="https://authzed.com/customers/openai" rel="noopener noreferrer"&gt;AuthZed, OpenAI customer story (SpiceDB)&lt;/a&gt; · &lt;a href="https://research.google/pubs/pub48190/" rel="noopener noreferrer"&gt;Google, the Zanzibar paper&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>architecture</category>
      <category>authorization</category>
      <category>go</category>
    </item>
    <item>
      <title>HTMX in Production: What I Kept, What I Removed</title>
      <dc:creator>Jules Robineau</dc:creator>
      <pubDate>Fri, 18 Sep 2026 06:03:00 +0000</pubDate>
      <link>https://dev.to/julesrobineau/htmx-in-production-what-i-kept-what-i-removed-p44</link>
      <guid>https://dev.to/julesrobineau/htmx-in-production-what-i-kept-what-i-removed-p44</guid>
      <description>&lt;p&gt;HTMX articles come in two camps: "the end of SPAs" and "a toy". Production reports are the rare thing. Here is one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: my prospecting cockpit runs in production on HTMX, Go, &lt;code&gt;html/template&lt;/code&gt;, Postgres and Redis. What held up: one endpoint per fragment, templates split by component, Redis as an optional cache. What I removed: the SSE push, which froze the page on HTTP/1.1. And htmx 4.0 just shipped, which says something about the approach's maturity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article is for backend developers who want a real interface without adopting a full frontend framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The cockpit is my freelance prospecting tool. Conversation tracking, monitoring, lead analysis. I use it every day, and it runs in production.&lt;/p&gt;

&lt;p&gt;The stack is short. Go with &lt;code&gt;net/http&lt;/code&gt;. &lt;code&gt;html/template&lt;/code&gt; templates. HTMX in the browser. Postgres for data, Redis as a cache.&lt;/p&gt;

&lt;p&gt;HTMX is a small JavaScript library. It reads HTML attributes and swaps parts of the page with HTML returned by the server. No frontend build, no npm, no client-side state.&lt;/p&gt;

&lt;p&gt;The bet: all the logic stays in Go, the browser only displays.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that emerged: one endpoint, one fragment
&lt;/h2&gt;

&lt;p&gt;At first, my templates were monoliths. The base template weighed 102 KB. Two pages were above 70 KB each.&lt;/p&gt;

&lt;p&gt;It works, but nobody dares touch it anymore. The refactor imposed a simple rule: an HTMX endpoint returns one fragment, and every fragment lives in its own file.&lt;/p&gt;

&lt;p&gt;A dedicated &lt;code&gt;fragments/&lt;/code&gt; folder. Pages split by component. The base CSS went from 1,256 lines to 192, with a per-page file included automatically.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;html/template&lt;/code&gt; holds up very well, on one condition: split it the way you would split your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I removed: the SSE push
&lt;/h2&gt;

&lt;p&gt;I had built real-time. A Redis pub/sub channel, an events endpoint, a toast in the page when an analysis finished. Elegant on paper.&lt;/p&gt;

&lt;p&gt;In practice, the page showed a never-ending loading state. An SSE stream is an HTTP request that never finishes. On HTTP/1.1, that permanent stream occupies one of the browser's few connection slots to your origin.&lt;/p&gt;

&lt;p&gt;I covered this family of traps in &lt;a href="https://jrobineau.com/blog/2026-08-24-go-timeouts-sse-http2.html" rel="noopener noreferrer"&gt;my article on Go timeouts and SSE&lt;/a&gt;. This time it was my own app.&lt;/p&gt;

&lt;p&gt;The real question was not "how do I fix it". It was "who needs this real-time". Honest answer: nobody. An HTMX refresh on click gives the same information.&lt;/p&gt;

&lt;p&gt;The SSE left in one commit. Real-time is a permanent cost, not a gift. Pay it only when the need exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I kept on the server side
&lt;/h2&gt;

&lt;p&gt;Redis stayed, but as a cache only. Matching embeddings are cached with a TTL. Same for the most re-read account data.&lt;/p&gt;

&lt;p&gt;The important part: everything is optional. The Redis adapter is nil-safe, and the cockpit starts and works without Redis, just slower. An optional dependency is one less outage.&lt;/p&gt;

&lt;p&gt;The job queue stayed in Postgres. Durable, transactional, already there. The pair, Postgres for state and Redis for speed, covers everything an app like this needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  htmx 4.0 just shipped, and it matters
&lt;/h2&gt;

&lt;p&gt;On 28 August 2026, htmx 4.0 was released. The engine moves from XMLHttpRequest to fetch, the browser's modern request API. The project skipped version 3, as promised.&lt;/p&gt;

&lt;p&gt;What I take away is not the technical novelty. It is the signal: the hypermedia approach is maintained, funded, and built to last.&lt;/p&gt;

&lt;p&gt;I have not migrated yet. My usage is deliberately simple, core attributes and fragments. The migration will wait for a quiet slot, and that is exactly what I want from a frontend dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I recommend this stack to, and who not
&lt;/h2&gt;

&lt;p&gt;Go for it if you are a small backend team with an internal tool, a back office or a CRUD-heavy app. The payoff is huge: one codebase, one deployment, zero frontend pipeline.&lt;/p&gt;

&lt;p&gt;Think twice if your product lives on rich client-side interactions. Collaborative editing, offline, complex animations: there, a frontend framework earns its keep.&lt;/p&gt;

&lt;p&gt;And if you already have a frontend team fluent in its framework, HTMX is not an upgrade. It is a different trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist if you try HTMX
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;The rules I would apply from day one, in order.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;[ ] An HTMX endpoint returns one fragment, and every fragment lives in its own file&lt;/li&gt;
&lt;li&gt;[ ] Split your templates by component before they reach 100 KB&lt;/li&gt;
&lt;li&gt;[ ] Extract per-page CSS, loaded automatically by convention&lt;/li&gt;
&lt;li&gt;[ ] Do not add real-time without a real need: a refresh on click is often enough&lt;/li&gt;
&lt;li&gt;[ ] If you do SSE, first read how it behaves on HTTP/1.1&lt;/li&gt;
&lt;li&gt;[ ] Make dependencies optional: the app must start without the cache&lt;/li&gt;
&lt;li&gt;[ ] Keep durable state in Postgres and cache in Redis, not the other way around&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to remember
&lt;/h2&gt;

&lt;p&gt;HTMX in production is mostly template discipline and deliberate server-side choices. The library itself fades into the background.&lt;/p&gt;

&lt;p&gt;My verdict after months of daily use: I keep the stack, I removed the excess, and htmx 4 reassures me.&lt;/p&gt;

&lt;p&gt;Want a solid interface without a frontend factory, or a second opinion on your Go web stack? &lt;a href="https://jrobineau.com/contact.html" rel="noopener noreferrer"&gt;Let's talk&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Sources:&lt;/strong&gt; &lt;a href="https://four.htmx.org/announcements/2026-08-28-htmx-4.0.0-is-released" rel="noopener noreferrer"&gt;htmx 4.0 announcement (28 August 2026)&lt;/a&gt; · &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;htmx.org&lt;/a&gt; · &lt;a href="https://pkg.go.dev/html/template" rel="noopener noreferrer"&gt;Go html/template (pkg.go.dev)&lt;/a&gt; · &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;MDN, Server-sent events&lt;/a&gt;&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>go</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
