<?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: Leo</title>
    <description>The latest articles on DEV Community by Leo (@leobaniak).</description>
    <link>https://dev.to/leobaniak</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%2F3997121%2F061e76e1-0a08-4d9d-827c-24f1e8b9e15b.png</url>
      <title>DEV Community: Leo</title>
      <link>https://dev.to/leobaniak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leobaniak"/>
    <language>en</language>
    <item>
      <title>Terraform 1.16 adds destroy-time Actions and lets child modules own their imports</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Tue, 06 Oct 2026 10:24:10 +0000</pubDate>
      <link>https://dev.to/leobaniak/terraform-116-adds-destroy-time-actions-and-lets-child-modules-own-their-imports-3b1j</link>
      <guid>https://dev.to/leobaniak/terraform-116-adds-destroy-time-actions-and-lets-child-modules-own-their-imports-3b1j</guid>
      <description>&lt;p&gt;I once kept a shell script called &lt;code&gt;pre-destroy.sh&lt;/code&gt; in a repo for far longer than I'd like to admit. It took a final backup, poked an external inventory system, and only then let the pipeline run &lt;code&gt;terraform destroy&lt;/code&gt;. Everyone on the team was a little scared of it, and nobody ever fully trusted it. Terraform 1.16 lets a lot of that logic move into the configuration, where the plan can see it.&lt;/p&gt;

&lt;p&gt;HashiCorp's release post covers two headline changes. Actions can now fire on &lt;code&gt;before_destroy&lt;/code&gt; and &lt;code&gt;after_destroy&lt;/code&gt; events. Import blocks can also live inside child modules, so the root module no longer has to own every import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Destroy gets its own hooks
&lt;/h2&gt;

&lt;p&gt;The release post opens with the cases you'd expect: taking a final backup, deregistering an asset, or cleaning up an external system before the infrastructure goes away. You attach an Action to a resource through &lt;code&gt;action_trigger&lt;/code&gt; in its &lt;code&gt;lifecycle&lt;/code&gt; block and list the destroy event in &lt;code&gt;events&lt;/code&gt;. The shape from HashiCorp's example looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="s2"&gt;"example_cleanup"&lt;/span&gt; &lt;span class="s2"&gt;"archive"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resource_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"example_service"&lt;/span&gt; &lt;span class="s2"&gt;"payments"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"payments"&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;action_trigger&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;events&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;before_destroy&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example_cleanup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive&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;For anyone who has wired up cleanup steps in a pipeline, this is the nice part. The cleanup step sits next to the resource it belongs to, it shows up at plan time, and you stop relying on whoever runs the destroy job to remember the wrapper script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Imports that travel with the module
&lt;/h2&gt;

&lt;p&gt;The second change is quieter, and I think more people will feel it day to day. Until now, an import block could target a resource inside a child module, but the block itself had to sit in the root module. HashiCorp calls that "an awkward exception" for modules that are supposed to hide their implementation details.&lt;/p&gt;

&lt;p&gt;In 1.16 the module can carry its own import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"existing_bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"this"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;existing_bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;this&lt;/span&gt;
  &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;existing_bucket_name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer passes a name and doesn't need to know any resource addresses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/log-bucket"&lt;/span&gt;
  &lt;span class="nx"&gt;existing_bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;existing-bucket-name&amp;gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform evaluates the import in the context of each module instance, and per the post that includes nested modules and module calls that use &lt;code&gt;count&lt;/code&gt; or &lt;code&gt;for_each&lt;/code&gt;. If you maintain a shared module catalogue, adoption of existing infrastructure becomes something the module author designs once instead of every consumer working it out alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rough edges you'll hit first
&lt;/h2&gt;

&lt;p&gt;Three things stood out to me as the ones that will trip up a pipeline.&lt;/p&gt;

&lt;p&gt;The configuration and trigger condition of a destroy Action must be fully known at plan time. If your cleanup inputs only resolve during apply, you'll need to restructure before this works for you.&lt;/p&gt;

&lt;p&gt;You can't just delete the resource block. Removing the block also removes its &lt;code&gt;action_trigger&lt;/code&gt;, so Terraform has nothing to invoke when it plans the destroy. HashiCorp's guidance is to set &lt;code&gt;count = 0&lt;/code&gt; on the resource and its trigger configuration first, then remove the block in a later change. That turns one pull request into two, so tell your reviewers why.&lt;/p&gt;

&lt;p&gt;Failure behaviour needs an explicit decision. The modes are &lt;code&gt;halt&lt;/code&gt; (the default, which stops dependent processing after an Action error), &lt;code&gt;continue&lt;/code&gt; (errors become warnings) and &lt;code&gt;taint&lt;/code&gt;. The post is careful to note that &lt;code&gt;taint&lt;/code&gt; marks a newly created resource for replacement and does not make a failed destroy Action retriable. If your backup step fails during a teardown, &lt;code&gt;continue&lt;/code&gt; lets the destroy go ahead anyway. Choose that one on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things in the same release
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;terraform state show -json&lt;/code&gt; and &lt;code&gt;terraform workspace list -json&lt;/code&gt; give scripts machine-readable output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform graph -format=mermaid&lt;/code&gt; produces a diagram you can drop into a pull request description.&lt;/li&gt;
&lt;li&gt;The CLI output includes an HCP Terraform policy evaluation summary.&lt;/li&gt;
&lt;li&gt;Linux &lt;code&gt;s390x&lt;/code&gt; binaries are now available.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How this compares with other tools
&lt;/h2&gt;

&lt;p&gt;Teardown hooks are an old idea. Terraform itself has long had destroy-time provisioners (&lt;code&gt;when = destroy&lt;/code&gt;), which run shell commands as a side effect of the destroy. They work, but a provisioner is an escape hatch and reviewers tend to treat it as one. AWS CloudFormation handles the backup case declaratively with &lt;code&gt;DeletionPolicy: Snapshot&lt;/code&gt; on supported resources, and uses custom resources for arbitrary cleanup on delete. Kubernetes uses finalizers, which block deletion of an object until a controller has done its cleanup. Terraform's version stands out to me for where it puts the decision: the cleanup appears in the plan and has a documented failure mode, so a reviewer can see it before anything is destroyed.&lt;/p&gt;

&lt;p&gt;The plan-time constraint and the two-step removal mean my old &lt;code&gt;pre-destroy.sh&lt;/code&gt; won't disappear overnight. Still, I'd move the backup step into an Action on the next module I touch. Next I'll be watching how teams handle &lt;code&gt;continue&lt;/code&gt; in production teardowns, because that's where a well-meant shortcut will turn into a missing backup. If you've already put destroy Actions in a pipeline, I'd like to hear how you handled the failure mode choice.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>terraform116</category>
      <category>hashicorp</category>
      <category>actions</category>
    </item>
    <item>
      <title>A semicolon in a Codex branch name leaked its GitHub token. Scope decided the damage</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Sat, 03 Oct 2026 10:24:21 +0000</pubDate>
      <link>https://dev.to/leobaniak/a-semicolon-in-a-codex-branch-name-leaked-its-github-token-scope-decided-the-damage-32o9</link>
      <guid>https://dev.to/leobaniak/a-semicolon-in-a-codex-branch-name-leaked-its-github-token-scope-decided-the-damage-32o9</guid>
      <description>&lt;p&gt;A branch name with a semicolon in it was enough to pull a GitHub OAuth token out of OpenAI's Codex, according to a DevOps.com write-up of a critical flaw that BeyondTrust's Phantom Labs disclosed in March. OpenAI has fixed it. For teams wiring agents into their delivery pipelines, the bug matters less than the token. Its scope decided how far a single injected command could reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;When Codex created a task container, it passed the target branch name into a shell command without sanitizing it first. Bash interpreted characters such as &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;$()&lt;/code&gt; and backticks as shell syntax rather than as part of a name.&lt;/p&gt;

&lt;p&gt;The proof of concept was short. Set the branch to &lt;code&gt;main&lt;/code&gt;, add a semicolon to end the intended git command, then append a second command that writes the output of &lt;code&gt;git remote get-url origin&lt;/code&gt; to a file. That remote URL held the GitHub OAuth token in cleartext. The researchers then asked the agent, in the prompt, to read the file back. Codex read it, and the token came back in the agent's own task output.&lt;/p&gt;

&lt;p&gt;The write-up says the flaw reached every Codex surface: the ChatGPT web interface, the CLI, the SDK and the IDE extension. Researchers confirmed it could be automated to compromise multiple users sharing a repository. OpenAI spent roughly six weeks on iterative hardening before the issue was classified Critical and cleared for public disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blast radius is a provisioning choice
&lt;/h2&gt;

&lt;p&gt;The DevOps.com piece describes each wired-up agent as a new privileged identity. The agent clones a real repository and authenticates with a real GitHub credential, but it gets less scrutiny than a human with the same access would. A token limited to one branch of one repo turns this injection into an inconvenience. A token with broad organizational access turns it into an org-wide GitHub compromise that started in a coding assistant.&lt;/p&gt;

&lt;p&gt;The article backs this with survey data. Teleport's 2026 State of AI in Enterprise Infrastructure Security report, built on interviews with 205 CISOs and security architects, found that organizations over-provisioning AI systems see 4.5 times more security incidents than those enforcing least privilege. In the same report, 70% said they give AI agents more access than a human doing the identical task, and 67% still use static credentials for AI systems. Both Teleport and Gravitee, whose separate survey the article also cites, sell access and security products in this space. Treat the figures as directional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening the input path
&lt;/h2&gt;

&lt;p&gt;Git allows &lt;code&gt;;&lt;/code&gt;, &lt;code&gt;$&lt;/code&gt;, parentheses, &lt;code&gt;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt; and backticks in ref names, so &lt;code&gt;git check-ref-format&lt;/code&gt; will not reject this payload. The agent harness has to do it. The article's list of untrusted fields covers branch names, file paths, commit messages and ticket titles. Any of them becomes command injection once it reaches a subprocess unsanitized.&lt;/p&gt;

&lt;p&gt;The narrowest fix is to stop building shell strings. If a shell is unavoidable, allowlist the characters, reject anything that looks like an option, and quote the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# $BRANCH and $REPO_URL arrive from the task request; treat them as hostile&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  -&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;[!&lt;/span&gt;A-Za-z0-9._/-]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"rejecting branch name"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1 &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REPO_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Moving the token is not enough
&lt;/h2&gt;

&lt;p&gt;Moving the token out of the remote URL and into a credential helper or a git config header changes the file that holds it. Any command running as the agent's user can still read it. What limits the damage is scope and lifetime. The article's checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope the credential to the task, not to the developer who configured the agent.&lt;/li&gt;
&lt;li&gt;Prefer short-lived, single-use credentials over static tokens, so a theft is limited to one task.&lt;/li&gt;
&lt;li&gt;Ask for actual visibility into what the agent's credential can do right now, as a list of repos and scopes, not a policy document.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CI has already seen this bug class
&lt;/h2&gt;

&lt;p&gt;None of this is new to pipeline operators. GitHub's Actions hardening guidance has long warned against interpolating attacker-controlled values such as &lt;code&gt;github.head_ref&lt;/code&gt; or pull request titles directly into &lt;code&gt;run:&lt;/code&gt; scripts, and it recommends passing them through an environment variable. GitHub App installation tokens can be restricted to selected repositories and permissions, and they expire after an hour. GitLab's &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; is valid only while its job runs. OIDC federation replaces stored cloud keys with per-job tokens.&lt;/p&gt;

&lt;p&gt;Agent harnesses do not inherit any of that by default. The Codex sanitization bug is closed. How far the next leaked agent token reaches depends on how the agent was provisioned, and that decision belongs to whoever configured it.&lt;/p&gt;

</description>
      <category>codingagents</category>
      <category>commandinjection</category>
      <category>githubtoken</category>
      <category>leastprivilege</category>
    </item>
    <item>
      <title>npm trusted publishing finally covers dist-tags, if you ask nicely</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:25:20 +0000</pubDate>
      <link>https://dev.to/leobaniak/npm-trusted-publishing-finally-covers-dist-tags-if-you-ask-nicely-3582</link>
      <guid>https://dev.to/leobaniak/npm-trusted-publishing-finally-covers-dist-tags-if-you-ask-nicely-3582</guid>
      <description>&lt;p&gt;Picture the npm maintainer who did the hard work: moved publishing to OIDC, deleted the automation token from the password manager, celebrated with coffee. Then shipped a patch and had to run &lt;code&gt;npm dist-tag add&lt;/code&gt; from a laptop because the shiny new trusted publishing flow could push versions but not point &lt;code&gt;latest&lt;/code&gt; at them. Awkward.&lt;/p&gt;

&lt;p&gt;That gap closed this week. On 2026-09-30, GitHub announced that npm trusted publishing can now manage dist-tags through the same short-lived OIDC credentials it already uses to publish, instead of requiring a long-lived access token sitting somewhere unpleasant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the opt-in actually does
&lt;/h2&gt;

&lt;p&gt;The announcement is narrow and specific. Each trusted publishing configuration on an npm package gets a new permission, labelled &lt;strong&gt;Allow npm dist-tag&lt;/strong&gt;. Turn it on, and workflows authenticating with that configuration can promote a version to &lt;code&gt;latest&lt;/code&gt; or move the &lt;code&gt;next&lt;/code&gt; and &lt;code&gt;beta&lt;/code&gt; pointers from CI, authenticated by the same OIDC token that would have published a version.&lt;/p&gt;

&lt;p&gt;A few details worth pinning down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The permission is &lt;strong&gt;off by default&lt;/strong&gt; on both brand-new configurations and the ones you set up months ago. Nothing in your current setup silently gained new rights.&lt;/li&gt;
&lt;li&gt;It is independent of direct publishing permissions. A staging-only configuration can be granted dist-tag management without also being allowed to publish, and vice versa.&lt;/li&gt;
&lt;li&gt;Authorization triggers when an incoming OIDC token matches any configuration with the permission on.&lt;/li&gt;
&lt;li&gt;Classic token-based dist-tag management keeps working. If you have a cron job somewhere that still uses an automation token, nothing changes for it today.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why dist-tags are the loose brick
&lt;/h2&gt;

&lt;p&gt;Publishing a version is only half of a release. The other half is pointing &lt;code&gt;latest&lt;/code&gt; at it, because &lt;code&gt;npm install your-pkg&lt;/code&gt; with no range resolves against &lt;code&gt;latest&lt;/code&gt;. Whoever can rewrite &lt;code&gt;latest&lt;/code&gt; can quietly aim an entire ecosystem at a different artifact.&lt;/p&gt;

&lt;p&gt;That is the lateral-move path supply-chain attackers love. Steal a publish token and you can publish &lt;code&gt;your-pkg@6.6.6&lt;/code&gt;, sure. But if you can also run &lt;code&gt;npm dist-tag add your-pkg@6.6.6 latest&lt;/code&gt;, every unpinned install now pulls your version until a human notices. Historically that second step still needed a classic token, which meant most teams kept one around specifically for retagging, defeating the point of OIDC for publishing in the first place.&lt;/p&gt;

&lt;p&gt;With dist-tag ops on OIDC, the "we moved to trusted publishing but kept one legacy token for tagging" footnote finally goes away, assuming you flip the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on without turning it loose
&lt;/h2&gt;

&lt;p&gt;Two decisions sit under that single checkbox.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which configurations get the power.&lt;/strong&gt; The ergonomic failure mode is to tick &lt;strong&gt;Allow npm dist-tag&lt;/strong&gt; on every configuration, including the one your preview workflow uses, because somebody will eventually need to retag during an incident and it is easier to pre-grant. That reproduces the problem OIDC was supposed to solve: broadly-scoped standing authority. The safer shape is a dedicated release configuration (bound to a specific workflow, a protected environment, maybe a manual approval) that is the only one allowed to retag, with publish-only configurations handling day-to-day versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What triggers it.&lt;/strong&gt; Because the permission matches on the OIDC token's claims (&lt;code&gt;repository&lt;/code&gt;, &lt;code&gt;workflow&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt;, and friends), what you are really doing is deciding which Actions runs are allowed to call the retag endpoint. Pin the configuration to a specific workflow file at a specific path and gate that workflow on an environment with required reviewers. The checkbox is only as strict as the token claims you check it against.&lt;/p&gt;

&lt;p&gt;A worth-saying caveat: existing dist-tag automation that still relies on a long-lived token is not disabled by any of this. Shutting that path off is a separate decision you have to make, usually by revoking the token after you have moved the automation across. Shipping the new permission does not retire the old one for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  How other registries and platforms handle release tags
&lt;/h2&gt;

&lt;p&gt;Dist-tags are an npm concept, but "mint a short-lived credential in CI, use it to update a release pointer" is a pattern now, not a feature. A rough map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PyPI trusted publishing&lt;/strong&gt; was the reference implementation. It covers uploads, and effectively covers "which version pip installs by default" because PyPI picks the highest non-yanked version, with no mutable tag in the middle. Different model, same primitive: OIDC from your CI, no long-lived token sitting in a secret store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RubyGems trusted publishing&lt;/strong&gt; mirrors PyPI closely: gem push authenticated via OIDC from a GitHub Actions workflow bound to specific claims. Yanks and ownership changes are a separate authority there; the retag analogue is less of an issue because the "default" version is computed, not pointed-at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitLab CI&lt;/strong&gt; reaches npm by federating its own ID tokens to the registry's OIDC trust. It is the better fit if your source of truth is a GitLab project and you want the publishing job's claims (&lt;code&gt;project_path&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, protected branch) to flow into the configuration you tie to the package. The new dist-tag permission applies the same way, because the gate is on npm's side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CircleCI OIDC&lt;/strong&gt; can do the same against npm's trust configuration, matching on CircleCI-specific claims. Honest trade-off: if your release workflow already lives on GitHub Actions, there is no real reason to add CircleCI to the path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins&lt;/strong&gt; does not get OIDC federation for free. Teams usually bolt on a plugin that mints an OIDC token the runner can present, or (more commonly) still ship a classic token into the agent. If you are on self-hosted Jenkins and this new permission matters to you, the honest answer is that the work is upstream of the checkbox.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buddy&lt;/strong&gt; is one option if you want the OIDC-emitting job and the retag step to live in the same place as the rest of your delivery pipeline, and you want environment-scoped approvals around a &lt;code&gt;npm dist-tag&lt;/code&gt; action instead of inside a workflow file. A minimal shape:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# buddy.yml, illustrative shape only&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Promote&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latest"&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BUILD"&lt;/span&gt;
  &lt;span class="na"&gt;docker_image_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node"&lt;/span&gt;
  &lt;span class="na"&gt;docker_image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lts"&lt;/span&gt;
  &lt;span class="na"&gt;trigger_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ON_EVERY_PUSH"&lt;/span&gt;
  &lt;span class="na"&gt;trigger_conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;trigger_condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VAR_IS"&lt;/span&gt;
      &lt;span class="na"&gt;trigger_variable_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GIT_REF"&lt;/span&gt;
      &lt;span class="na"&gt;trigger_variable_value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refs/tags/v*"&lt;/span&gt;
  &lt;span class="na"&gt;execute_commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;whoami"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dist-tag&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;add&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;$PACKAGE@$VERSION&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point of listing six options is that none of them is "the best" in the abstract. Which one you pick is a function of where your source of truth already lives, how your approval model is shaped, and how much appetite you have for owning a plugin to make OIDC work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable bit
&lt;/h2&gt;

&lt;p&gt;The press release reads like a win, and it is. One less long-lived token in one less secret store is one less thing to rotate and one less thing to steal. But opt-in defaults mean the ecosystem benefit lands when maintainers actually open the settings panel and tick the box, not on 2026-09-30. Until then, every package that previously held an automation token for tagging is still holding it, OIDC or not.&lt;/p&gt;

&lt;p&gt;Go turn it on. Then go delete the token.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>trustedpublishing</category>
      <category>oidc</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>GitLab ships critical patch across 19.4, 19.3 and 19.2</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Tue, 29 Sep 2026 10:24:52 +0000</pubDate>
      <link>https://dev.to/leobaniak/gitlab-ships-critical-patch-across-194-193-and-192-50ac</link>
      <guid>https://dev.to/leobaniak/gitlab-ships-critical-patch-across-194-193-and-192-50ac</guid>
      <description>&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;GitLab pushed a critical patch release on 23 September covering three active branches at once: 19.4.1, 19.3.3 and 19.2.7. The release notes on docs.gitlab.com bundle fixes rated from Critical through Low, and the maintainers strongly recommend that self-managed installations upgrade. Two of the disclosed issues carry the Critical label. Several fixes are Enterprise Edition only; most apply to both CE and EE.&lt;/p&gt;

&lt;p&gt;The operationally awkward detail is the age of the affected code. A couple of the vulnerabilities reach back to the 13.x and 15.x lines, meaning any instance sitting on an older LTS-style branch is exposed to bugs that predate the current major by years. The release notes offer no partial mitigation. The fix is the upgrade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs to absorb
&lt;/h2&gt;

&lt;p&gt;Multi-node instances can take the patch with zero downtime by rolling nodes through the standard update procedure. Single-node installs cannot: the release notes state the patch causes downtime, so a change window is required. That one sentence is the whole planning story for most teams running GitLab on a single host, and it decides whether the upgrade happens tonight or waits for the next scheduled window.&lt;/p&gt;

&lt;p&gt;For pipeline owners the read is narrower. A GitLab upgrade window locks the runner fleet's control plane, so long-running deploys, scheduled jobs and any merge trains queued against that instance need to drain or be paused. Critical-rated CVEs shorten the argument between the security team's clock and on-call's change freeze. Operators still on 19.2 or 19.3 have a second question to answer before booking the outage: whether to take just the branch patch, or ride the upgrade all the way to 19.4.1 while the window is already open.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>security</category>
      <category>cve</category>
      <category>patch</category>
    </item>
    <item>
      <title>The right fix for double-tap zoom is one CSS line, not a viewport lock</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Sat, 26 Sep 2026 16:10:08 +0000</pubDate>
      <link>https://dev.to/leobaniak/the-right-fix-for-double-tap-zoom-is-one-css-line-not-a-viewport-lock-6lc</link>
      <guid>https://dev.to/leobaniak/the-right-fix-for-double-tap-zoom-is-one-css-line-not-a-viewport-lock-6lc</guid>
      <description>&lt;p&gt;There is a category of "accessibility fix" that fixes nothing and locks people out. Disabling zoom on the whole page to stop one button from misbehaving is the classic example. If a user rapid-taps a control on your radio player, your podcast list, your carousel of station cards, and the page yanks itself larger under their thumb, the answer is not to take zoom away from everyone who needs it. The answer is to tell that one button to stop treating a fast second tap as a gesture.&lt;/p&gt;

&lt;p&gt;Andy Bell ran into this on a web radio player he was trying on his phone. Flicking rapidly through stations to find one on air, iOS Safari read the fast successive taps as its double-tap-to-zoom gesture, and the page zoomed in and out under his fingers. The write-up is short. The fix is shorter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reflex fix costs more than it saves
&lt;/h2&gt;

&lt;p&gt;You have seen the workaround before. Something in the UI misbehaves under fast taps, and someone reaches for the viewport meta tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That kills every zoom gesture on the whole page. Not just the double-tap on the button. Pinch, too. A user with low vision cannot enlarge the text. A user of a magnifier extension has nothing left to work with. Adding &lt;code&gt;maximum-scale=1.0, user-scalable=no&lt;/code&gt; to the viewport is called out in Andy's write-up as a WCAG violation, and it has been called out as one for as long as mobile Safari has shipped. The bug is fixed; the page is broken for the people who most need it to work.&lt;/p&gt;

&lt;p&gt;If your accessibility patch takes an ability away from every visitor to stop one gesture on one control, you have shipped a regression.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scope the opt-out to the control
&lt;/h2&gt;

&lt;p&gt;The tool Andy reaches for is &lt;code&gt;touch-action&lt;/code&gt;, applied directly to the button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;touch-action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;manipulation&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;That is the whole change. Per MDN, &lt;code&gt;touch-action: manipulation&lt;/code&gt; is an alias for &lt;code&gt;pan-x pan-y pinch-zoom&lt;/code&gt;. The browser keeps single-finger panning. It keeps two-finger pinch to zoom. What it drops is the browser's own set of extra gestures, most notably double-tap to zoom. A rapid second tap on a control that carries &lt;code&gt;manipulation&lt;/code&gt; fires as a second click, not as a zoom.&lt;/p&gt;

&lt;p&gt;There is a small side benefit. &lt;code&gt;touch-action: manipulation&lt;/code&gt; also removes the click-event delay that browsers historically inserted after a tap so they could disambiguate a first tap from an incoming double tap. On a control that has opted out of the second-tap gesture, there is nothing to wait for.&lt;/p&gt;

&lt;p&gt;Two things to notice about this fix, both of them accessibility properties in their own right:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It only affects the element you put it on. The rest of the page, headings and body copy and the whole reading surface, still zooms normally.&lt;/li&gt;
&lt;li&gt;It targets the browser's page-level zoom gestures on that one element, not the ability to zoom itself. Pinch stays.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is what an opt-out should look like: applied to the control that has a reason to opt out, and no wider.&lt;/p&gt;

&lt;h2&gt;
  
  
  For your next PR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If a control misfires on fast taps, put &lt;code&gt;touch-action: manipulation&lt;/code&gt; on that control, not on the page.&lt;/li&gt;
&lt;li&gt;Do not touch the viewport meta to fix a per-button problem. &lt;code&gt;maximum-scale=1.0&lt;/code&gt; and &lt;code&gt;user-scalable=no&lt;/code&gt; are the anti-pattern here, not the escape hatch.&lt;/li&gt;
&lt;li&gt;Leave pinch zoom alone. It is a feature for the people who need it to read your interface, not a defect for you to close.&lt;/li&gt;
&lt;li&gt;If a viewport lock is already sitting in a shared template somewhere, this is a good week to open the PR that pulls it.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>css</category>
      <category>touchaction</category>
      <category>iossafari</category>
    </item>
    <item>
      <title>GitLab's per-user email token can push code and trigger pipelines</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:24:27 +0000</pubDate>
      <link>https://dev.to/leobaniak/gitlabs-per-user-email-token-can-push-code-and-trigger-pipelines-208n</link>
      <guid>https://dev.to/leobaniak/gitlabs-per-user-email-token-can-push-code-and-trigger-pipelines-208n</guid>
      <description>&lt;p&gt;Every GitLab user carries a credential most of them have never looked at. It sits inside a per-project email address, it can push code and run pipelines, and it does not expire. Aikido Security researcher Joe Leon disclosed this week, in reporting on DevOps.com, that if that address leaks an attacker gets a fine-grained personal access token in convenient email form.&lt;/p&gt;

&lt;p&gt;The mechanic, per Leon's writeup: GitLab lets you create issues and merge requests by emailing a project. The token inside those private addresses is the same one across every project the user can reach. Change the suffix from "-issue" to "-merge-request", attach a .patch file, and you are submitting code as that user. Which means CI/CD runs. Which means CI_JOB_TOKEN. Which means whatever else that job can touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the token actually unlocks
&lt;/h2&gt;

&lt;p&gt;Leon lists the reachable surface as pushing patches, kicking off CI/CD pipelines, pulling private source, reading CI/CD variables and secrets, and pivoting to other GitLab resources via CI_JOB_TOKEN. Anywhere the account owner has access, the emailed token has access. GitLab's own documentation, quoted in the article, calls it "essentially a fine-grained personal access token with significant access to your GitLab projects." Read that sentence twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitLab's response, and yours
&lt;/h2&gt;

&lt;p&gt;GitLab has not turned the feature off. Aikido's HackerOne report in May was closed as intended behavior. A confidential issue followed in June. On July 28 GitLab opened a merge request updating the docs so the capability of the token is described more plainly. The interface got a clarification pass. That is the fix.&lt;/p&gt;

&lt;p&gt;The user-side mitigation is one click: reset the token in personal access token settings and every previously issued project address dies with it. Do that on any account whose email has ever appeared in a leak, a screenshot, a stale CI log or a support ticket. Rotate on a schedule if you can stomach the churn.&lt;/p&gt;

&lt;p&gt;A long-lived credential that grants pipeline write access, transmitted over email by design. Working as intended.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>cicdsecurity</category>
      <category>tokens</category>
      <category>supplychain</category>
    </item>
    <item>
      <title>Overlap detection lands in CSS with anchors and a shared timeline</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Fri, 25 Sep 2026 16:08:28 +0000</pubDate>
      <link>https://dev.to/leobaniak/overlap-detection-lands-in-css-with-anchors-and-a-shared-timeline-1460</link>
      <guid>https://dev.to/leobaniak/overlap-detection-lands-in-css-with-anchors-and-a-shared-timeline-1460</guid>
      <description>&lt;p&gt;Two elements share a row. A big word mark on the left, a section on the right. Resize the window and at some width they crash into each other. You know the shape of the fix. A &lt;code&gt;ResizeObserver&lt;/code&gt;, a class toggle, some hand-drawn threshold in JavaScript. On ishadeed.com, Ahmad Shadeed argues you can do the whole thing in CSS.&lt;/p&gt;

&lt;p&gt;The move has three parts, and each one leans on a modern primitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anchor two elements, then measure the gap between them
&lt;/h2&gt;

&lt;p&gt;Start with anchor positioning. The decoration gets &lt;code&gt;anchor-name: --line&lt;/code&gt;. The section that might collide with it gets &lt;code&gt;anchor-name: --section&lt;/code&gt;. That is the opening move. Two names, so the rest of the stylesheet can point at these boxes by handle rather than by DOM position.&lt;/p&gt;

&lt;p&gt;Now the trick. A third element, called &lt;code&gt;.measure&lt;/code&gt;, is positioned to fill the horizontal space between the two anchors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.measure&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--line&lt;/span&gt; &lt;span class="nb"&gt;right&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--section&lt;/span&gt; &lt;span class="nb"&gt;left&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;Read that literally. Its left edge sits at the right edge of the line. Its right edge sits at the left edge of the section. When the layout has room, &lt;code&gt;.measure&lt;/code&gt; has width. When the two collide, that width collapses to zero. You have turned overlap into a geometry problem you can point CSS at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn size changes into animation progress
&lt;/h2&gt;

&lt;p&gt;Anchor positioning gets the measurement. CSS still has to react to it. That is the scroll-driven animations piece.&lt;/p&gt;

&lt;p&gt;The source wires one element with &lt;code&gt;scroll-timeline: --box&lt;/code&gt; and drives another with &lt;code&gt;animation-timeline: --box&lt;/code&gt;. Once that link exists, the second element's animation is bound to the first element's scroll progress. Changing what is happening on one box becomes progress on somebody else's animation.&lt;/p&gt;

&lt;p&gt;That is the small, boring miracle here. Two mechanisms designed for different jobs, anchor positions and scroll timelines, meet in the middle and become an overlap listener.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why timeline-scope is the piece that makes it composable
&lt;/h2&gt;

&lt;p&gt;There is a catch, and it is the reason the article exists. A named scroll-timeline is scoped to the element that defines it. Only that element and its descendants can see the name. If the thing you want to animate is a sibling, or lives higher up the tree, the name is invisible to it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;timeline-scope&lt;/code&gt; fixes that. Put it on a common ancestor and the named scroll-timeline is lifted up to that ancestor's scope, where any descendant can consume it. That is what makes the pattern actually usable. You can put the measurement inside the layout and wire the reaction, hiding the decoration or swapping a color, anywhere in the subtree.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;timeline-scope&lt;/code&gt;, you would be architecturally stuck. With it, the pieces snap together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it changes on your next layout
&lt;/h2&gt;

&lt;p&gt;A caveat straight from the source: the demos need a browser that supports both scroll-driven animations and CSS anchor positioning. This is not a technique you can hand every visitor tomorrow.&lt;/p&gt;

&lt;p&gt;The design question is the bigger one. For years, "react to layout" meant reaching for &lt;code&gt;ResizeObserver&lt;/code&gt; and &lt;code&gt;IntersectionObserver&lt;/code&gt;. Those APIs are fine, but they push layout-aware behaviour into JavaScript, where it competes with everything else on the main thread. The pattern in this article does not. The measurement is a positioned element. The reaction is an animation timeline. The wiring is a scoped name.&lt;/p&gt;

&lt;p&gt;The interesting next question is not "when does this ship everywhere" — it is what else you have been solving in JavaScript that fits this same shape. Collision. Gap thresholds. Container state that is really about geometry, not media features. Anywhere you were listening to &lt;code&gt;resize&lt;/code&gt; to change a class, there is now a CSS-native path worth trying first.&lt;/p&gt;

</description>
      <category>css</category>
      <category>anchorpositioning</category>
      <category>scrolldrivenanimations</category>
      <category>timelinescope</category>
    </item>
    <item>
      <title>Safari Technology Preview 253 lets container queries branch on sibling-index() and frees style-originated scroll timelines</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:07:53 +0000</pubDate>
      <link>https://dev.to/leobaniak/safari-technology-preview-253-lets-container-queries-branch-on-sibling-index-and-frees-3dgk</link>
      <guid>https://dev.to/leobaniak/safari-technology-preview-253-lets-container-queries-branch-on-sibling-index-and-frees-3dgk</guid>
      <description>&lt;p&gt;I opened the notes this morning, and the first item made me stop scrolling. &lt;code&gt;sibling-index()&lt;/code&gt; and &lt;code&gt;sibling-count()&lt;/code&gt; inside a &lt;code&gt;@container&lt;/code&gt; condition. That's a small change on paper and a real shift in what container queries can answer.&lt;/p&gt;

&lt;p&gt;Safari Technology Preview 253 is out for macOS Golden Gate and macOS Tahoe, and covers WebKit changes 320113@main through &lt;a href="mailto:321067@main"&gt;321067@main&lt;/a&gt;. Preview channel, one engine. The release does not claim Baseline for anything below, and I won't extrapolate one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change that reframes container queries
&lt;/h2&gt;

&lt;p&gt;Per the release, &lt;code&gt;@container&lt;/code&gt; conditions can now use &lt;code&gt;sibling-index()&lt;/code&gt; and &lt;code&gt;sibling-count()&lt;/code&gt; — change &lt;a href="mailto:320493@main"&gt;320493@main&lt;/a&gt;. Container queries have always answered "how wide is my container", "how tall is my container". They now answer a different kind of question: "where am I in the list I belong to, and how long is that list".&lt;/p&gt;

&lt;p&gt;That's a category shift. Position among peers used to be an &lt;code&gt;:nth-child&lt;/code&gt; job on the child selector side of the cascade, or a script that walked the DOM and set data attributes. Neither approach fed a container query. Now the condition itself can carry that fact, and the rule body can respond to it.&lt;/p&gt;

&lt;p&gt;The release lists the change and the WebKit revision. It does not print a full example. I am not going to invent one — if you are trying it in STP tonight, the shape you want to try is a &lt;code&gt;@container&lt;/code&gt; block whose condition tests one of the two functions, and let the parser tell you what it accepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scroll-timeline scope, opened up
&lt;/h2&gt;

&lt;p&gt;Style-originated scroll timelines now match globally, per change &lt;a href="mailto:320183@main"&gt;320183@main&lt;/a&gt;. Read that carefully: they can be defined outside the target's ancestor hierarchy, or outside an element that carries a &lt;code&gt;timeline-scope&lt;/code&gt; property. Before this change, a timeline had to sit somewhere the animated element could see through the tree. That constraint was the reason authors were reaching for &lt;code&gt;timeline-scope&lt;/code&gt; in the first place, to hoist visibility up to a common ancestor.&lt;/p&gt;

&lt;p&gt;The release frames the change as "match globally". If you had a scroll-driven animation working only because you had rearranged the DOM to put the timeline in the right ancestor, that plumbing can start coming out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the CSS and OM surface
&lt;/h2&gt;

&lt;p&gt;Two smaller items round out the release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CSSContainerRule.conditions&lt;/code&gt; is now supported (change 320762@main). The condition text of a &lt;code&gt;@container&lt;/code&gt; rule is readable off the CSSOM, so tooling that inspects stylesheets can ask a rule for its own condition instead of parsing selectors by hand.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ViewTimeline&lt;/code&gt; constructor now correctly requires a &lt;code&gt;subject&lt;/code&gt; parameter (change 320969@main). If your script was constructing a &lt;code&gt;ViewTimeline&lt;/code&gt; without one and getting away with it, that call will need to change. A tightening of the JavaScript API to match the specification, not a new capability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  An accessibility fix worth reading twice
&lt;/h2&gt;

&lt;p&gt;Change 320424@main is a VoiceOver correction. When &lt;code&gt;aria-keyshortcuts&lt;/code&gt; values contain &lt;code&gt;Meta&lt;/code&gt; or &lt;code&gt;Alt&lt;/code&gt;, VoiceOver was announcing those tokens literally, instead of the macOS terminology &lt;code&gt;Command&lt;/code&gt; and &lt;code&gt;Option&lt;/code&gt;. Users heard "Meta" for the key labelled Command on the keyboard in front of them.&lt;/p&gt;

&lt;p&gt;That's the kind of quiet correctness fix that only surfaces when you sit with a screen reader and listen. If you author custom shortcuts on interactive components, this is one to re-run through VoiceOver once the fix reaches a channel you can test against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm watching next
&lt;/h2&gt;

&lt;p&gt;Two things. First, whether other engines pick up &lt;code&gt;sibling-index()&lt;/code&gt; and &lt;code&gt;sibling-count()&lt;/code&gt; in &lt;code&gt;@container&lt;/code&gt; conditions — cross-engine parity is what turns a preview curiosity into something you can lean on in a design system. Second, what authors actually do with style-originated timelines now that they escape the ancestor hierarchy: the interesting demos will be the ones that would have needed &lt;code&gt;timeline-scope&lt;/code&gt; gymnastics before, and now do not.&lt;/p&gt;

&lt;p&gt;Nothing here has a Baseline claim. STP is what STP has always been: try the syntax, keep the fallback path intact, watch the other engines.&lt;/p&gt;

</description>
      <category>safari</category>
      <category>webkit</category>
      <category>stp</category>
      <category>css</category>
    </item>
    <item>
      <title>The CrowdSec leak is a lesson in the OAuth token you forgot to revoke</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Thu, 24 Sep 2026 10:25:17 +0000</pubDate>
      <link>https://dev.to/leobaniak/the-crowdsec-leak-is-a-lesson-in-the-oauth-token-you-forgot-to-revoke-197i</link>
      <guid>https://dev.to/leobaniak/the-crowdsec-leak-is-a-lesson-in-the-oauth-token-you-forgot-to-revoke-197i</guid>
      <description>&lt;p&gt;Somewhere in your GitHub organization, a former employee still holds an active OAuth authorization for your private repositories. You just have not audited the list this year. And when the next npm supply-chain event fires, that grant is what walks out of a poisoned build and clones your source.&lt;/p&gt;

&lt;p&gt;This is not hypothetical. DevOps.com reported on September 24 that CrowdSec, the French threat-intelligence outfit, had source code stolen from about 170 of its private GitHub repositories after an OAuth token tied to a former employee's GitHub account was scraped by attackers who had first compromised the TanStack npm packages. The attacker group calling itself TeamPCP cloned the archive in a window the report clocks at roughly nine minutes. The code eventually surfaced on a dark-web leak forum months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain, one step at a time
&lt;/h2&gt;

&lt;p&gt;Walk the sequence end to end, because the interesting failure is not where you might expect.&lt;/p&gt;

&lt;p&gt;Step one, per the report: in May, TeamPCP pushed malicious artifacts into TanStack's npm packages using a self-propagating worm the writeup labels "Mini Shai-Hulud." The worm's job was not to sabotage your build. Its job was to sit inside your build and rifle through the environment for credentials and tokens.&lt;/p&gt;

&lt;p&gt;Step two: one of the developers whose install pulled a poisoned TanStack package happened to be a former CrowdSec employee. Their personal GitHub account still carried an OAuth authorization for CrowdSec's organization, with read access to private repositories. The worm scooped that token alongside everything else in reach.&lt;/p&gt;

&lt;p&gt;Step three: attackers cloned. About 170 private repositories from a larger set of public and private ones, downloaded in a nine-minute burst that the report attributes to scripts rather than a human at a keyboard. CrowdSec revoked the former employee's access days later. The archive was already elsewhere, and it turned up on a leak forum in September.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OAuth-app ledger nobody audits
&lt;/h2&gt;

&lt;p&gt;Read that chain again. The npm worm is the loud, interesting part. The pivot into CrowdSec's private repositories was almost boring: it used an OAuth grant nobody had reviewed since the employee's exit interview.&lt;/p&gt;

&lt;p&gt;An OAuth token issued to a personal GitHub account is a bearer credential. It does not know the employee left. It does not care that HR closed the ticket on their last working day. It sits in your organization's list of authorized OAuth apps until someone revokes it explicitly or the third-party app is uninstalled, and a &lt;code&gt;repo&lt;/code&gt; scope covers every private repository the account had access to on the day the grant was signed.&lt;/p&gt;

&lt;p&gt;This is the CI/CD security question that hides in plain sight. Every team drills SSO offboarding. Fewer teams script OAuth-app offboarding. Almost nobody drills a scenario where a third party's build is the compromise and the pivot vector is a token they issued three quarters earlier for a plugin nobody uses anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually check this week
&lt;/h2&gt;

&lt;p&gt;Some of these are dull. Do them anyway.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Export the list of OAuth apps authorized in your GitHub organization, then cross-reference the user accounts against current employees. Every mismatch is a suspect.&lt;/li&gt;
&lt;li&gt;Enforce OAuth-app approval at the organization level so an individual contributor cannot silently authorize a new tool against &lt;code&gt;repo&lt;/code&gt; scope.&lt;/li&gt;
&lt;li&gt;Where the tooling supports it, prefer GitHub Apps with fine-grained repository permissions over user-scoped OAuth apps. GitHub Apps live at the organization level and survive the employee, cleanly.&lt;/li&gt;
&lt;li&gt;Rotate anything a poisoned CI or laptop build could have touched: &lt;code&gt;.npmrc&lt;/code&gt; tokens, cached cloud credentials, SSH keys copied into agent workspaces, personal access tokens sitting in shell dotfiles.&lt;/li&gt;
&lt;li&gt;Treat every ecosystem-scale npm compromise as a credential-rotation event for anyone whose laptop or pipeline installed that package family in the affected window. Not only the developer who noticed something odd. Everyone.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where the industry is heading
&lt;/h2&gt;

&lt;p&gt;Some ecosystems are sanding this problem down. GitHub's own push toward fine-grained personal access tokens and short-lived OIDC-brokered credentials in Actions targets the same shape: a token you cannot forget, because it expired ninety minutes after the workflow finished. GitLab and Bitbucket have moved along the same axis with workload-identity federation into the major cloud providers. The pattern is consistent across vendors. Replace long-lived bearer tokens with something the pipeline mints on demand and the identity provider revokes on its own timer.&lt;/p&gt;

&lt;p&gt;None of that retires the OAuth-app grants sitting in your organization tonight. That surface predates the newer models. It is nobody's favourite thing to review, which is exactly why it is where the attackers went.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kicker
&lt;/h2&gt;

&lt;p&gt;Your offboarding checklist ends where your OAuth-app inventory begins. Go look at it. You will not like what you find.&lt;/p&gt;

</description>
      <category>supplychain</category>
      <category>oauth</category>
      <category>github</category>
      <category>npm</category>
    </item>
    <item>
      <title>Chrome 154 ships responsively-sized iframes with a two-side opt-in</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Wed, 23 Sep 2026 16:08:25 +0000</pubDate>
      <link>https://dev.to/leobaniak/chrome-154-ships-responsively-sized-iframes-with-a-two-side-opt-in-2029</link>
      <guid>https://dev.to/leobaniak/chrome-154-ships-responsively-sized-iframes-with-a-two-side-opt-in-2029</guid>
      <description>&lt;p&gt;I wired this into a comment widget this morning and the handshake is exactly what the writeup promised. Chrome 154 gives &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; a new CSS property called &lt;code&gt;frame-sizing&lt;/code&gt;, and the embedded page has one job: drop a &lt;code&gt;&amp;lt;meta name="responsive-embedded-sizing"&amp;gt;&lt;/code&gt; tag in its &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;. The outer page and the embed agree, the frame takes on the size of its content, and nothing has to postMessage a pixel count across the origin boundary. Per Bram's post dated 23 September 2026, only Chromium ships it. Firefox and Safari have no support and no tracking bugs yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two sides of the handshake
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;frame-sizing&lt;/code&gt; goes on the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; and accepts &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;content-height&lt;/code&gt;, &lt;code&gt;content-width&lt;/code&gt;, plus logical-property variants. That is the embedder's request: size yourself to your content's intrinsic height, width, or both. On its own it does nothing. The embedded page has to opt in with a meta tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"responsive-embedded-sizing"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"allow-origins=*"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;allow-origins&lt;/code&gt; value can be a list of origins instead of &lt;code&gt;*&lt;/code&gt;, which is how the embed picks who is allowed to receive its size information. If your comment widget is dropped into twenty sites but you only want two of them to auto-size, that is the knob.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest working wire-up
&lt;/h2&gt;

&lt;p&gt;Outer page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;iframe&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://comments.example/thread/42"&lt;/span&gt;
        &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"frame-sizing: auto"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the embed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"responsive-embedded-sizing"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"allow-origins=*"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is it. No ResizeObserver. No script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telling the parent you grew
&lt;/h2&gt;

&lt;p&gt;Content shifts after load. Someone opens a reply, an image finishes decoding, a "load more" runs. When that happens the embedded document calls &lt;code&gt;window.requestResize()&lt;/code&gt; and the outer page picks up the new size through the same channel. One method call replaces the postMessage-plus-parseInt-plus-parent-style-write recipe most embeds carry around today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cross-browser reality
&lt;/h2&gt;

&lt;p&gt;Here is the part I have to keep honest. Per the browser-support table in Bram's post on 23 September, &lt;code&gt;frame-sizing&lt;/code&gt; and &lt;code&gt;responsive-embedded-sizing&lt;/code&gt; are Chromium-only right now. Firefox has no support and no tracking bug. Safari has no support and no tracking bug. So this is progressive enhancement in the strictest sense: shipping it today upgrades one engine and leaves the others exactly where they were. If you already keep a postMessage fallback for embed heights, keep it. If you do not, the frame renders at whatever &lt;code&gt;height&lt;/code&gt; and &lt;code&gt;width&lt;/code&gt; you set on it, same as always.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am watching next
&lt;/h2&gt;

&lt;p&gt;Whether Gecko and WebKit file tracking bugs at all, and how &lt;code&gt;allow-origins&lt;/code&gt; behaves in the field. A cross-origin size channel is exactly the kind of surface that tightens as embedders test it against ad frames and third-party widgets. I also want to see whether the property picks up more values once authors put pressure on the shape.&lt;/p&gt;

</description>
      <category>css</category>
      <category>iframe</category>
      <category>chrome</category>
      <category>framesizing</category>
    </item>
    <item>
      <title>The Codex sandbox escapes prove your agent's blast radius is your laptop</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Tue, 22 Sep 2026 10:24:56 +0000</pubDate>
      <link>https://dev.to/leobaniak/the-codex-sandbox-escapes-prove-your-agents-blast-radius-is-your-laptop-1g5a</link>
      <guid>https://dev.to/leobaniak/the-codex-sandbox-escapes-prove-your-agents-blast-radius-is-your-laptop-1g5a</guid>
      <description>&lt;p&gt;Your coding agent is running untrusted patches inside a sandbox on the same laptop that holds your SSH keys, your cloud tokens, your kubeconfig, and the GitHub credentials that push signed commits into production. Take a moment with that mental image. Comfortable? Good.&lt;/p&gt;

&lt;p&gt;On September 15, 2026, Accomplish AI published the technical write-up of two sandbox escapes in OpenAI Codex. Researcher Oren Yomtov named them Heapjack and Overpatch. Both let code jump out of the Codex sandbox and run on the developer host with no approval prompt and nothing on screen. Reported to OpenAI on August 12, patched within eight days. Fast triage, and OpenAI earned that credit. The uncomfortable part is that the shape of the bug is going to keep happening, because the sandbox lives inside the thing it is supposed to contain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanics, kept to what the disclosure says
&lt;/h2&gt;

&lt;p&gt;Heapjack and Overpatch had different root causes, per Accomplish's writeup, but the same outcome: arbitrary code executing on the developer's machine outside the approval flow that Codex normally shows for shell commands, network calls and file writes. No dialog fired. No log line surfaced. A user watching the agent work would have seen the same interface they always see, while a payload from a fetched dependency or a poisoned test fixture was already running as them.&lt;/p&gt;

&lt;p&gt;Two independent escapes, one system, one review cycle. That is the number that matters. Not because OpenAI is uniquely careless (they patched within a week and a day), but because the sandbox model everyone in this space uses is a probabilistic bet against a determined attacker and a large surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "on the same machine" is the wrong trust boundary
&lt;/h2&gt;

&lt;p&gt;Look at what a coding agent inherits when it runs on your laptop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SSH keys that push to your git host.&lt;/li&gt;
&lt;li&gt;The keychain entries and short-lived tokens that open pull requests, merge changes and kick off CI runs.&lt;/li&gt;
&lt;li&gt;Cloud credentials cached under &lt;code&gt;~/.aws&lt;/code&gt;, &lt;code&gt;~/.config/gcloud&lt;/code&gt;, &lt;code&gt;~/.kube&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm&lt;/code&gt;, PyPI and container-registry publish tokens.&lt;/li&gt;
&lt;li&gt;Whatever &lt;code&gt;.envrc&lt;/code&gt; you loaded three sessions ago and forgot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A sandbox escape in this context is not "the agent ran a stray command." It is "attacker-controlled code inherited your full developer identity, including the credentials that sign commits and trigger production deployments." That is a supply-chain path that starts on the laptop and ends in the release pipeline, wearing a legitimate signature the whole way.&lt;/p&gt;

&lt;p&gt;GitHub's 2026 Actions security roadmap already cites &lt;code&gt;tj-actions/changed-files&lt;/code&gt;, Nx and &lt;code&gt;trivy-action&lt;/code&gt; as recent pipeline-targeting incidents. Those all travelled through the CI system itself. Heapjack and Overpatch open a second lane, one that skips CI entirely and compromises the human whose keys CI trusts. If your threat model stops at the runner, you now have a gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approaches that move the blast radius somewhere less painful
&lt;/h2&gt;

&lt;p&gt;There is no free lunch here, but there are architectures that put the compromise domain somewhere other than your developer identity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Container isolation&lt;/strong&gt; wraps the agent in a namespace boundary. It shares the kernel, and any workspace mount usually punches a convenience hole straight back through the boundary. It stops the trivial cases and nothing more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;microVM sandboxes&lt;/strong&gt; give the agent its own kernel. This is the direction production CI runners have moved for the same reason: strong isolation cheap enough to spin up per task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote execution&lt;/strong&gt; moves the whole runtime off the developer machine. The agent runs in a hosted workspace reached over an API, so an escape reaches a stateless VM the vendor recycles, not your keyring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these costs something. Remote execution loses low-latency filesystem work. microVMs cost RAM and start-up time. Containers still hand you a shared kernel. Pick your poison, but pick one that fails into a domain you can rotate cheaply.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change on Monday
&lt;/h2&gt;

&lt;p&gt;You cannot wait for every agent vendor to redesign their sandbox. In the meantime:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assume any coding agent on your laptop can eventually reach anything your shell can. Move long-lived tokens out of the environment and into a keychain that requires per-use approval.&lt;/li&gt;
&lt;li&gt;Prefer agents that run in a remote or containerised workspace when the task allows it. Accept the latency.&lt;/li&gt;
&lt;li&gt;Rotate credentials after any agent session that used shell tools or fetched network content. Yes, all of them.&lt;/li&gt;
&lt;li&gt;Push CI toward OIDC-brokered short-lived credentials, so a stolen laptop token cannot pivot into production.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The kicker
&lt;/h2&gt;

&lt;p&gt;Every agent vendor will tell you the sandbox is safe. Right up until the writeup goes live and the patch note ships. Trust the boundary, never the brochure.&lt;/p&gt;

</description>
      <category>supplychainsecurity</category>
      <category>codingagents</category>
      <category>codex</category>
      <category>sandboxescape</category>
    </item>
    <item>
      <title>Container queries observe the component, not the viewport</title>
      <dc:creator>Leo</dc:creator>
      <pubDate>Sat, 19 Sep 2026 16:08:17 +0000</pubDate>
      <link>https://dev.to/leobaniak/container-queries-observe-the-component-not-the-viewport-3chn</link>
      <guid>https://dev.to/leobaniak/container-queries-observe-the-component-not-the-viewport-3chn</guid>
      <description>&lt;p&gt;Drop a card component into a two-column layout. In the wide column it lays out as a row: image left, copy right. Move the same card into a 320px sidebar and it still tries to be a row, because its breakpoint was written against the viewport, not the box it now lives in. That is the pattern Victor Ayomipo, writing in Smashing Magazine on 16 September, is trying to get you to stop shipping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each query lives
&lt;/h2&gt;

&lt;p&gt;Media queries answer the browser. Container queries answer the box.&lt;/p&gt;

&lt;p&gt;Ayomipo frames it as "macro" vs "micro" layout. A media query asks the viewport how wide the page is. A container query asks a specific ancestor element how wide it is. Reuse the same breakpoints across both and the component reads the wrong signal.&lt;/p&gt;

&lt;p&gt;The State of CSS numbers he cites explain why the mismatch keeps happening: 86% of developers know container queries exist, and 41.4% actually use them. Browser support sits at roughly 94% (caniuse.com), so the gap is not the engines. It's the habit of copying media-query shapes into new syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-line wiring
&lt;/h2&gt;

&lt;p&gt;The minimum shown in the source to make a card respond to its own width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card-wrapper&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;container-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;container-type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inline-size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@container&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;450px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.card&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&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;&lt;code&gt;container-type: inline-size&lt;/code&gt; gives the wrapper a size a descendant can query. &lt;code&gt;container-name&lt;/code&gt; labels it. Then &lt;code&gt;@container card (...)&lt;/code&gt; reads that wrapper's inline size, not the viewport. Drop the card in a 300px rail: the row layout stays off. Drop it in a 600px column: it flips. Same component, two contexts.&lt;/p&gt;

&lt;p&gt;One rule to internalise, per the article: you cannot query the element you are styling. The component and its container have to be different nodes, or the layout goes into a loop. That is the reason for the wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container units, same idea
&lt;/h2&gt;

&lt;p&gt;Container units are the other half. Ayomipo's typography example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.card-title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;.5rem&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;cqi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2rem&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;&lt;code&gt;cqi&lt;/code&gt; is 1% of the container's inline size. Wired to a &lt;code&gt;container-type: inline-size&lt;/code&gt; ancestor, &lt;code&gt;clamp()&lt;/code&gt; gives the title a floor, a ceiling, and a smooth ramp between them driven by the box rather than the viewport. The card grows and shrinks with its column. No media-query stack, no per-breakpoint font sizes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rough edges
&lt;/h2&gt;

&lt;p&gt;The article walks through a flex-wrap detection pattern for a card grid: flex items sized &lt;code&gt;flex: 1 1 390px&lt;/code&gt; are each given &lt;code&gt;container-type: inline-size&lt;/code&gt;, then an &lt;code&gt;@container (min-width: 600px)&lt;/code&gt; rule flips a card from column to row once it stops sharing its row with siblings. The card notices when it is alone. The viewport is not involved.&lt;/p&gt;

&lt;p&gt;Ayomipo also flags the sharp corners. Querying block size can collapse a layout when the container has no explicit height. Custom-property values do not work inside a container-query condition, so a shared &lt;code&gt;--breakpoint&lt;/code&gt; variable is out. And the wrapper element is real DOM; a component still cannot query itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to try next
&lt;/h2&gt;

&lt;p&gt;Pick one component on your current project that reads viewport width — a card, a media object, a summary tile. Wrap it, set &lt;code&gt;container-type: inline-size&lt;/code&gt; on the wrapper, and rewrite its breakpoints as &lt;code&gt;@container&lt;/code&gt; rules with &lt;code&gt;cqi&lt;/code&gt; for the type ramp. Drop it in three contexts on the same page. If it renders the same layout in a 300px rail and a 900px column, the breakpoints are still viewport-shaped. Kevin Powell, quoted from a SmashingConf Amsterdam 2026 talk, called container-query adoption "terrible" for exactly that reason.&lt;/p&gt;

</description>
      <category>css</category>
      <category>containerqueries</category>
      <category>responsivedesign</category>
      <category>layout</category>
    </item>
  </channel>
</rss>
