<?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: Indra Gusti Prasetya</title>
    <description>The latest articles on DEV Community by Indra Gusti Prasetya (@indra_gustiprasetya_a80a).</description>
    <link>https://dev.to/indra_gustiprasetya_a80a</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%2F3971045%2F76516018-d46d-403b-9d79-239ac1d80baa.png</url>
      <title>DEV Community: Indra Gusti Prasetya</title>
      <link>https://dev.to/indra_gustiprasetya_a80a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/indra_gustiprasetya_a80a"/>
    <language>en</language>
    <item>
      <title>Fix conflicting SELinux labels of volume in K8s 1.37</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Fri, 28 Aug 2026 04:42:57 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-conflicting-selinux-labels-of-volume-in-k8s-137-3oho</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-conflicting-selinux-labels-of-volume-in-k8s-137-3oho</guid>
      <description>&lt;p&gt;Kubernetes 1.37 shipped on 26 August 2026, and with it &lt;code&gt;SELinuxMount&lt;/code&gt; went stable and on by default. A qualifying PersistentVolume is now handed to the container runtime with &lt;code&gt;-o context=&amp;lt;label&amp;gt;&lt;/code&gt; instead of being walked inode by inode. On a volume with four million files, that converts minutes of pod startup into milliseconds, which is the reason KEP-1710 exists in the first place. It also means a mounted volume carries exactly one SELinux label for as long as it stays mounted, and two pods with different labels can no longer share it on the same node.&lt;/p&gt;

&lt;p&gt;The second pod sits in &lt;code&gt;ContainerCreating&lt;/code&gt;. Kubelet emits an event naming the conflict, phrased in &lt;a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/1710-selinux-relabeling/README.md" rel="noopener noreferrer"&gt;KEP-1710&lt;/a&gt; as &lt;em&gt;"volume XYZ is already mounted with a different SELinux label"&lt;/em&gt;, and described in the docs as conflicting SELinux labels of a volume, with the other pod's name attached when both live in the same namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which pods actually take the new mount path?
&lt;/h2&gt;

&lt;p&gt;Four conditions, all of them, per the &lt;a href="https://kubernetes.io/docs/tasks/configure-pod-container/security-context/" rel="noopener noreferrer"&gt;security-context task docs&lt;/a&gt;: the pod uses a PersistentVolumeClaim; &lt;code&gt;spec.securityContext.seLinuxChangePolicy&lt;/code&gt; is nil or &lt;code&gt;MountOption&lt;/code&gt;; the containers using that PVC set &lt;code&gt;seLinuxOptions&lt;/code&gt;; and the PV is an in-tree &lt;code&gt;iscsi&lt;/code&gt;, &lt;code&gt;rbd&lt;/code&gt;, or &lt;code&gt;fc&lt;/code&gt; volume, or a CSI volume whose CSIDriver sets &lt;code&gt;spec.seLinuxMount: true&lt;/code&gt; (a field that has been available since 1.25). Fail any gate and you get 1.36 behaviour.&lt;/p&gt;

&lt;p&gt;The uncomfortable consequence is who qualifies. A pod that never sets &lt;code&gt;seLinuxOptions&lt;/code&gt; is untouched. The pods that pass all four gates are the ones that deliberately pin a label: sidecar-injected workloads, OpenShift-style deployments, anything setting &lt;code&gt;seLinuxOptions.level&lt;/code&gt; so two workloads can share a volume on purpose. The teams that did the SELinux work are the ones this lands on, which puts it alongside the &lt;a href="///blog/fix-a-mirror-pod-may-not-reference-secrets-in-k8s-1-37.html"&gt;mirror pod secret reference break&lt;/a&gt; and the &lt;a href="///blog/stop-kubernetes-dra-giving-one-gpu-to-a-vm-and-a-pod.html"&gt;DRA GPU double-allocation hazard&lt;/a&gt; in the 1.37 category of "correct configuration is the entry condition for the failure".&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A["Pod with a PVC starts on a 1.37 node"] --&amp;gt; B{"seLinuxChangePolicy = Recursive?"}
  B --&amp;gt;|Yes| R["Runtime relabels every inode (1.36 behaviour)"]
  B --&amp;gt;|No| C{"Containers set seLinuxOptions?"}
  C --&amp;gt;|No| R
  C --&amp;gt;|Yes| D{"CSIDriver seLinuxMount: true?"}
  D --&amp;gt;|No| R
  D --&amp;gt;|Yes| E["kubelet mounts with -o context=label"]
  E --&amp;gt; F{"Volume already mounted on this node under another label?"}
  F --&amp;gt;|Yes| G["Pod stuck in ContainerCreating\nconflicting SELinux labels of volume"]
  F --&amp;gt;|No| H["Pod runs; chcon on that volume returns EOPNOTSUPP"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;One command scopes the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get csidriver &lt;span class="nt"&gt;-o&lt;/span&gt; custom-columns&lt;span class="o"&gt;=&lt;/span&gt;NAME:.metadata.name,SELINUXMOUNT:.spec.seLinuxMount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS EBS CSI and Ceph RBD both advertise &lt;code&gt;seLinuxMount: true&lt;/code&gt;, so most SELinux-enforcing fleets on RHEL, CoreOS, Bottlerocket, or Flatcar are in scope the moment the nodes reach 1.37. The &lt;a href="https://kubernetes-csi.github.io/docs/csi-driver-object.html" rel="noopener noreferrer"&gt;CSI driver object reference&lt;/a&gt; documents the field if you need to confirm what a third-party driver claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four post-upgrade symptoms that get blamed on the same thing
&lt;/h2&gt;

&lt;p&gt;Upgrade threads currently blend these together. They have different causes and different discriminating checks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom after upgrading to 1.37&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Check that proves it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pod stuck in &lt;code&gt;ContainerCreating&lt;/code&gt;, event names two SELinux labels&lt;/td&gt;
&lt;td&gt;Two labels, one &lt;code&gt;-o context=&lt;/code&gt; mount, same node&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;kubectl describe pod&lt;/code&gt;; node metric &lt;code&gt;volume_manager_selinux_volume_context_mismatch_errors_total&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing changed, startup still slow on large volumes&lt;/td&gt;
&lt;td&gt;A gate failed, usually &lt;code&gt;seLinuxMount: false&lt;/code&gt; on the driver or containers with no &lt;code&gt;seLinuxOptions&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;findmnt -o TARGET,OPTIONS&lt;/code&gt; on the node; no &lt;code&gt;context=&lt;/code&gt; in the options&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App runs, then &lt;code&gt;chcon&lt;/code&gt; / &lt;code&gt;restorecon&lt;/code&gt; / &lt;code&gt;setfattr&lt;/code&gt; fails with "Operation not supported"&lt;/td&gt;
&lt;td&gt;Kernel forbids per-file relabelling on mountpoint-labelled filesystems&lt;/td&gt;
&lt;td&gt;Reproduce with &lt;code&gt;chcon -t svirt_sandbox_file_t &amp;lt;file&amp;gt;&lt;/code&gt; inside the container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second pod on a shared NFS export silently gets the first pod's label&lt;/td&gt;
&lt;td&gt;Superblock reuse; KEP-1710 notes NFS needs &lt;code&gt;nosharecache&lt;/code&gt;, CIFS &lt;code&gt;nosharesock&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compare &lt;code&gt;context=&lt;/code&gt; in &lt;code&gt;findmnt&lt;/code&gt; against the pod's own &lt;code&gt;seLinuxOptions.level&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row three deserves a look at the source rather than the release notes. SELinux disables &lt;code&gt;setxattr&lt;/code&gt; on &lt;code&gt;security.selinux&lt;/code&gt; for filesystems labelled at the mountpoint, in &lt;code&gt;selinux_inode_setxattr()&lt;/code&gt; in &lt;code&gt;security/selinux/hooks.c&lt;/code&gt;, added by the commit &lt;a href="https://nv-tegra.nvidia.com/r/plugins/gitiles/linux-2.6/+/25a74f3ba8efb394e9a30d6de37566bf03fd3de8%5E%21/security/selinux/hooks.c" rel="noopener noreferrer"&gt;"selinux: disable setxattr on mountpoint labeled filesystems"&lt;/a&gt;. Red Hat's SELinux documentation states the user-visible result directly: &lt;code&gt;chcon&lt;/code&gt; on a filesystem mounted with a context option returns "Operation not supported". Any application that manages its own file labels on a PVC stops working on 1.37, and Kubernetes reports nothing at all, because from kubelet's point of view the pod started fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the recommended pre-flight check come up clean on EKS?
&lt;/h2&gt;

&lt;p&gt;The docs and the &lt;a href="https://kubernetes.io/blog/2026/04/22/breaking-changes-in-selinux-volume-labeling/" rel="noopener noreferrer"&gt;SIG-Storage announcement of 22 April 2026&lt;/a&gt; both point at the SELinuxWarningController. It watches running pods, emits an event on both sides of a conflict, and raises &lt;code&gt;selinux_warning_controller_selinux_volume_conflict&lt;/code&gt;. The docs also record that it ships disabled and is turned on with &lt;code&gt;--controllers=*,selinux-warning-controller&lt;/code&gt; on kube-controller-manager, with the &lt;code&gt;SELinuxChangePolicy&lt;/code&gt; gate (GA since 1.36).&lt;/p&gt;

&lt;p&gt;That is a control-plane flag. EKS, GKE, and AKS do not expose it. So the advice showing up in upgrade checklists, "check for conflict events before you upgrade", returns zero on managed clusters because no controller was ever counting. The reachable signal is per-node: scrape kubelet &lt;code&gt;/metrics&lt;/code&gt; for &lt;code&gt;volume_manager_selinux_volume_context_mismatch_warnings_total&lt;/code&gt;, which KEP-1710 describes as counting pods that would fail once &lt;code&gt;SELinuxMount&lt;/code&gt; is enabled. On a 1.36 fleet that counter is the honest forecast, and even then a zero only tells you the conflicting pair never happened to co-locate during the window you were scraping.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure is intermittent, because it depends on scheduling
&lt;/h2&gt;

&lt;p&gt;Both pods have to land on the same node for the conflict to fire. A ReadWriteMany PVC shared between a privileged log-shipper and an unprivileged app can pass a staging upgrade for weeks, then break the first time a descheduler run, a node drain, or a bin-packing change puts them together. Absence of failures on upgrade day is weak evidence. This is the same reasoning that makes cluster-wide guarantees like &lt;a href="///blog/kubernetes-default-deny-egress-stops-pod-exfiltration.html"&gt;default-deny egress&lt;/a&gt; worth more than per-workload assumptions: anything that only holds under a particular placement will eventually meet a different placement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blanket Recursive opt-out gives back the thing you upgraded for
&lt;/h2&gt;

&lt;p&gt;The circulating fix is &lt;code&gt;seLinuxChangePolicy: Recursive&lt;/code&gt;, pushed cluster-wide through MutatingAdmissionPolicy, Kyverno, or Gatekeeper. The docs suggest roughly that, and I would not apply it fleet-wide. Setting it everywhere returns every qualifying volume to full-tree relabelling, which is the startup cost KEP-1710 was written to remove, and on a large volume that is the difference between a pod starting in milliseconds and a pod starting in minutes. Scope it to the namespaces that genuinely share a volume across two labels, keep the mount path for everything else, and accept that you now have a per-namespace policy to maintain.&lt;/p&gt;

&lt;p&gt;There is a second-order problem with the opt-out that I have seen bite in a different guise: the field can be silently dropped. An API server older than 1.36 prunes &lt;code&gt;seLinuxChangePolicy&lt;/code&gt; from the pod spec, so the manifest in Git says &lt;code&gt;Recursive&lt;/code&gt; and the running pod says nothing. Schema mismatches between what you submit and what the API server retains are a recurring class of Kubernetes bug, the same shape as &lt;a href="///blog/fix-unrecognized-format-int32-in-kubernetes-1-34.html"&gt;"unrecognized format int32"&lt;/a&gt;. Confirm against the live object, not the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check before you promote 1.37 to production
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List the exposed drivers.&lt;/strong&gt; Run the &lt;code&gt;kubectl get csidriver&lt;/code&gt; command above. Any driver with &lt;code&gt;SELINUXMOUNT: true&lt;/code&gt; puts every PVC it serves in scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the conflicting pairs.&lt;/strong&gt; For each of those PVCs, list the pods that mount it and compare &lt;code&gt;seLinuxOptions.level&lt;/code&gt; across them. Differing levels on one PVC is the blast radius, and it is usually a short list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrape the forecast metric on every node&lt;/strong&gt; while still on 1.36: &lt;code&gt;volume_manager_selinux_volume_context_mismatch_warnings_total&lt;/code&gt; from kubelet &lt;code&gt;/metrics&lt;/code&gt;. Treat zero as unproven until you have confirmed from step 2 that a conflicting pair exists and has been co-located.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable the SELinuxWarningController&lt;/strong&gt; if you run your own control plane: &lt;code&gt;--controllers=*,selinux-warning-controller&lt;/code&gt;, with the &lt;code&gt;SELinuxChangePolicy&lt;/code&gt; gate. On EKS, GKE, and AKS, write in the upgrade plan that this check is unavailable and that the node metric is the substitute, so nobody later reads a clean event log as a pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;seLinuxChangePolicy: Recursive&lt;/code&gt; on the pairs from step 2 only&lt;/strong&gt;, then verify it persisted: &lt;code&gt;kubectl get pod -o jsonpath='{.spec.securityContext.seLinuxChangePolicy}'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep your images and charts for &lt;code&gt;chcon&lt;/code&gt;, &lt;code&gt;restorecon&lt;/code&gt;, and &lt;code&gt;setfattr&lt;/code&gt;&lt;/strong&gt; run against a PVC path. Those calls will start returning &lt;code&gt;EOPNOTSUPP&lt;/code&gt; on qualifying volumes, with no Kubernetes event to explain it. Reproduce with &lt;code&gt;chcon -t svirt_sandbox_file_t &amp;lt;file&amp;gt;&lt;/code&gt; inside the container before the upgrade, not after the pager goes off.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 take an afternoon on a mature cluster. Step 6 is the one teams skip, and it is the one that produces a silent application failure weeks later with no trace in &lt;code&gt;kubectl describe&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does SELinuxMount affect every pod with a PVC in Kubernetes 1.37?&lt;/strong&gt;&lt;br&gt;
No. All four gates must pass: PVC-backed volume, &lt;code&gt;seLinuxChangePolicy&lt;/code&gt; nil or &lt;code&gt;MountOption&lt;/code&gt;, &lt;code&gt;seLinuxOptions&lt;/code&gt; set on the containers using that PVC, and either an in-tree &lt;code&gt;iscsi&lt;/code&gt;/&lt;code&gt;rbd&lt;/code&gt;/&lt;code&gt;fc&lt;/code&gt; volume or a CSI volume whose CSIDriver sets &lt;code&gt;spec.seLinuxMount: true&lt;/code&gt;. Otherwise the runtime relabels recursively as before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;chcon&lt;/code&gt; fail with "Operation not supported" on a PVC after upgrading?&lt;/strong&gt;&lt;br&gt;
SELinux refuses &lt;code&gt;setxattr&lt;/code&gt; on &lt;code&gt;security.selinux&lt;/code&gt; for filesystems labelled at the mountpoint, in &lt;code&gt;selinux_inode_setxattr()&lt;/code&gt; in &lt;code&gt;security/selinux/hooks.c&lt;/code&gt;. Once kubelet mounts with &lt;code&gt;-o context=&lt;/code&gt;, per-file relabelling inside that volume is denied by the kernel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I opt a single workload out?&lt;/strong&gt;&lt;br&gt;
Set &lt;code&gt;spec.securityContext.seLinuxChangePolicy: Recursive&lt;/code&gt; on the pod, then confirm with &lt;code&gt;kubectl get pod -o jsonpath='{.spec.securityContext.seLinuxChangePolicy}'&lt;/code&gt;. An API server older than 1.36 can prune the value without error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use the SELinuxWarningController on a managed cluster?&lt;/strong&gt;&lt;br&gt;
No. It needs a kube-controller-manager flag that EKS, GKE, and AKS do not expose. Use &lt;code&gt;volume_manager_selinux_volume_context_mismatch_warnings_total&lt;/code&gt; from kubelet on each node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NFS and CIFS volumes get per-pod contexts?&lt;/strong&gt;&lt;br&gt;
Not by default. KEP-1710 notes NFS needs &lt;code&gt;nosharecache&lt;/code&gt; and CIFS needs &lt;code&gt;nosharesock&lt;/code&gt;, otherwise superblock reuse hands the second pod the label from the first mount.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-conflicting-selinux-labels-of-volume-in-k8s-1-37.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
    </item>
    <item>
      <title>Fix ESLint Cannot read properties of undefined (Intrinsic)</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:10:20 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-eslint-cannot-read-properties-of-undefined-intrinsic-fkp</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-eslint-cannot-read-properties-of-undefined-intrinsic-fkp</guid>
      <description>&lt;p&gt;Two teams hit the same wall this month with the same tool and needed opposite fixes. One upgraded to TypeScript 7 and forced past a peer range that had told them no. The other never touched TypeScript: their &lt;code&gt;package.json&lt;/code&gt; still pins &lt;code&gt;typescript: ^6.0.3&lt;/code&gt;, and their package manager wrote TypeScript 7 into the tree anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; ESLint throwing &lt;code&gt;Cannot read properties of undefined (reading 'Cjs')&lt;/code&gt; or &lt;code&gt;(reading 'Intrinsic')&lt;/code&gt; means typescript-eslint loaded TypeScript 7, whose Go-native rewrite dropped the JS enum exports it reads. Run &lt;code&gt;pnpm why typescript&lt;/code&gt; to see which of three trees you have. If the path runs through &lt;code&gt;@typescript-eslint/types&lt;/code&gt;, pnpm's bundled compatibility database put it there, and editing your own &lt;code&gt;package.json&lt;/code&gt; will not move it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two error strings, one missing enum
&lt;/h2&gt;

&lt;p&gt;The crashes look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'Cjs')
    at .../@typescript-eslint/typescript-estree/dist/create-program/shared.js:59:18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of undefined (reading 'Intrinsic')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both are the same read against a different missing export. &lt;code&gt;shared.js:59&lt;/code&gt; reaches for &lt;code&gt;.Cjs&lt;/code&gt; on a TypeScript enum object that came back &lt;code&gt;undefined&lt;/code&gt;, and the &lt;code&gt;'Intrinsic'&lt;/code&gt; variant does the same thing one enum over. TypeScript 7.0 went GA on 8 July 2026 as the Go-native compiler and shipped with no stable programmatic API, so the JS module every TypeScript major had exposed for a decade is no longer there in the shape the linter expects. This is the same class of failure as the &lt;a href="///blog/fix-npm-cannot-read-properties-of-null-edgesout.html"&gt;npm arborist &lt;code&gt;edgesOut&lt;/code&gt; crash&lt;/a&gt;: a null-property read that is really a resolution problem three layers up.&lt;/p&gt;

&lt;p&gt;The threads on this run the two cases together, which is why the popular fix works for half the people who try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where TypeScript 7 came from if you never installed it
&lt;/h2&gt;

&lt;p&gt;pnpm ships a &lt;strong&gt;compatibility database&lt;/strong&gt;: a table of &lt;code&gt;packageExtensions&lt;/code&gt; patches, maintained partly with Yarn as &lt;code&gt;@yarnpkg/extensions&lt;/code&gt; plus pnpm's own curated entries, that rewrites third-party &lt;code&gt;package.json&lt;/code&gt; files during resolution. One entry gave &lt;code&gt;@typescript-eslint/types&lt;/code&gt; a dependency on &lt;code&gt;typescript&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It started reasonably. &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/3622" rel="noopener noreferrer"&gt;typescript-eslint issue #3622&lt;/a&gt;, opened in 2021, correctly reported that &lt;code&gt;@typescript-eslint/types&lt;/code&gt; reaches for a &lt;code&gt;typescript&lt;/code&gt; module it never declares. The patch declared it, unpinned. For five years that resolved to whatever TypeScript major was current and nothing broke, because every major kept the same JS module surface. Then one did not.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0" rel="noopener noreferrer"&gt;pnpm 12.0.0 release notes&lt;/a&gt;, published 27 August, state the outcome directly: the removed entries "named packages that are only imported for their types, so installing them was at best unnecessary and at worst broke the dependent: &lt;code&gt;@typescript-eslint/types&lt;/code&gt; gained a &lt;code&gt;typescript&lt;/code&gt; dependency resolved to the newest release, which put TypeScript 7 under older &lt;code&gt;@typescript-eslint&lt;/code&gt; versions and made ESLint fail with 'Cannot read properties of undefined (reading Intrinsic)'."&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A["your package.json\ntypescript: ^6.0.3"] --&amp;gt; B["pnpm resolution"]
  B --&amp;gt; C["compatibility database\npackageExtensions"]
  C --&amp;gt;|"injects typescript dependency"| D["@typescript-eslint/types"]
  D --&amp;gt; E["typescript@7.x\nGo-native compiler"]
  E --&amp;gt; F["require('typescript')\ninside typescript-estree"]
  F --&amp;gt; G{"enum exports present?"}
  G --&amp;gt;|"no"| H["TypeError:\nreading 'Intrinsic'"]
  G --&amp;gt;|"yes"| I["lint runs"]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Which of the three trees do you have
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;pnpm why typescript&lt;/code&gt; (or &lt;code&gt;npm ls typescript&lt;/code&gt;) before you change a single line, and read the path.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What &lt;code&gt;pnpm why&lt;/code&gt; shows&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix that works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Path is your root &lt;code&gt;devDependencies&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;You upgraded and overrode the peer range&lt;/td&gt;
&lt;td&gt;Roll back to 6.x, or run TS 7 as a second non-blocking checker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path is &lt;code&gt;@typescript-eslint/types &amp;gt; typescript&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Compatibility database injected the edge&lt;/td&gt;
&lt;td&gt;pnpm 12.0.0, &lt;code&gt;overrides&lt;/code&gt;, or &lt;code&gt;ignoreCompatibilityDb=true&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two entries, TS 6 and TS 7 at different depths&lt;/td&gt;
&lt;td&gt;Hoisting resolution, common under &lt;code&gt;node-linker=hoisted&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Dedupe, or pin via &lt;code&gt;overrides&lt;/code&gt; so both depths agree&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the first row: &lt;code&gt;typescript-eslint@8.63.0&lt;/code&gt; declares a peer range of &lt;code&gt;&amp;gt;=4.8.4 &amp;lt;6.1.0&lt;/code&gt;. Getting past that took &lt;code&gt;--legacy-peer-deps&lt;/code&gt;, &lt;code&gt;--force&lt;/code&gt;, or &lt;code&gt;strictPeerDependencies=false&lt;/code&gt;. The range was accurate and the override turned a correct install-time refusal into a runtime TypeError several minutes into CI, which is the same trade the &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;npm 12 install-script lockdown&lt;/a&gt; taught people to stop making. A flag that silences the message rarely removes the condition.&lt;/p&gt;

&lt;p&gt;Then confirm which compiler the tool actually loaded, rather than the one you believe you installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"const ts=require('typescript'); [ts.version, typeof ts.Extension, typeof ts.ScriptKind].join(' | ')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;7.x&lt;/code&gt; version string with &lt;code&gt;undefined&lt;/code&gt; for both enum types is the signature. Those objects are exactly what &lt;code&gt;typescript-estree&lt;/code&gt; reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pin that does nothing
&lt;/h2&gt;

&lt;p&gt;The top-voted advice on every one of these threads is "pin &lt;code&gt;typescript&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt;" or "downgrade." On the injected path, the edge was written into a transitive package's manifest during resolution, so your root pin is not in the conversation. Neither is your &lt;code&gt;.pnpmfile.cjs&lt;/code&gt;: the compatibility database is applied after it, so your own transformations cannot see or override the patch.&lt;/p&gt;

&lt;p&gt;Three levers actually reach it, in the order I would try them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upgrade to pnpm 12.0.0.&lt;/strong&gt; It removes the statically-analysed entries while keeping the curated &lt;code&gt;@yarnpkg/extensions&lt;/code&gt; ones. Read the &lt;a href="///blog/pnpm-12-upgrade-fix-12-rust-cli-traps-before-ci-breaks.html"&gt;pnpm 12 upgrade traps&lt;/a&gt; first, because the Rust CLI rewrite lands several unrelated breaks in the same bump.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add an &lt;code&gt;overrides&lt;/code&gt; entry&lt;/strong&gt; pinning &lt;code&gt;typescript&lt;/code&gt; to your 6.x line. This wins over the injected version and works on any pnpm from 7 onward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set &lt;code&gt;ignoreCompatibilityDb=true&lt;/code&gt;&lt;/strong&gt; in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt;, available since pnpm 7.9.0. It works, and it disables every fixup in the table, including ones other packages in your tree quietly depend on. Use it to unblock a build today and upgrade this week.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  A patch table with no Git diff
&lt;/h2&gt;

&lt;p&gt;The interesting part of this failure is the supply-chain shape, and it deserves more attention than the workaround. A package manager added an edge to your dependency graph from a table compiled into its own binary. The edge does land in &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;, so it is technically visible, but it appears on a &lt;em&gt;pnpm version bump&lt;/em&gt; with no &lt;code&gt;package.json&lt;/code&gt; change, which is the diff every reviewer scrolls past.&lt;/p&gt;

&lt;p&gt;Rush's documentation has flagged the structure for years, calling the fixups &lt;a href="https://rushjs.io/pages/advanced/compatibility_db/" rel="noopener noreferrer"&gt;"hidden magic bundled into the PNPM binary with no Git diff visibility"&lt;/a&gt;, coupled to whichever &lt;code&gt;@yarnpkg/extensions&lt;/code&gt; snapshot that particular pnpm build happens to carry. Your dependency graph is a function of your lockfile &lt;em&gt;and&lt;/em&gt; your package manager build. If you generate an &lt;a href="///blog/generate-an-sbom-with-syft-filter-grype-with-vex.html"&gt;SBOM from the installed tree&lt;/a&gt;, it will faithfully record TypeScript 7 as a real component of your project, sourced from nothing you ever wrote down.&lt;/p&gt;

&lt;p&gt;Pin &lt;code&gt;packageManager&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; and treat a bump to it as a dependency change in review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually links against tsgo today
&lt;/h2&gt;

&lt;p&gt;There is a working path to type-aware linting on TypeScript 7 right now. oxc's &lt;code&gt;tsgolint&lt;/code&gt; went stable on 22 July 2026, tracks TypeScript v7.0.2, and implements 59 of typescript-eslint's 61 type-aware rules. The &lt;a href="https://oxc.rs/blog/2026-07-22-type-aware-linting-stable" rel="noopener noreferrer"&gt;oxc announcement&lt;/a&gt; reports 12-18x faster runs than ESLint with typescript-eslint across vscode, TypeScript, typeorm, and vuejs/core. It gets there by linking typescript-go directly instead of importing the JS module, which is why the API gap does not touch it.&lt;/p&gt;

&lt;p&gt;The cost is two missing rules and a different config surface. If lint wall-clock is your CI bottleneck, that is a cheap trade. If your rule set is heavily customised, budget real time for the migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 7.1 fixes on 10 November, and what it does not
&lt;/h2&gt;

&lt;p&gt;typescript-eslint closed the TypeScript 7 support request (&lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/12518" rel="noopener noreferrer"&gt;#12518&lt;/a&gt;) as not planned on GA day. The &lt;a href="https://github.com/microsoft/TypeScript/issues/63703" rel="noopener noreferrer"&gt;TypeScript 7.1 iteration plan&lt;/a&gt; lists "Stabilize API" covering Content Mapper, Emit, and Language Service, against a 7.1 stable target of &lt;strong&gt;2026-11-10&lt;/strong&gt;, roughly ten weeks out.&lt;/p&gt;

&lt;p&gt;That date unblocks the version range. It does not finish the job. Per &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/10940" rel="noopener noreferrer"&gt;typescript-eslint #10940&lt;/a&gt;, tsgo is consumed over native or WASM bindings, ESLint core has no async parser support, and rules expect JS-land AST nodes. A stable API gives the maintainers something to build against; ESLint's synchronous rule model still has to meet a Go type checker somewhere in the middle. Plan on 7.x type-aware linting through the official parser arriving well after November.&lt;/p&gt;

&lt;p&gt;The blast radius extends past ESLint for the same reason. &lt;code&gt;ts-jest&lt;/code&gt;, &lt;code&gt;ts-morph&lt;/code&gt;, API Extractor, and the template checkers behind Volar, Svelte, and Astro all consume that missing programmatic API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this before your next CI run
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;pnpm why typescript&lt;/code&gt;. Read the path and match it against the table above. Everything else is downstream of knowing which tree you have.&lt;/li&gt;
&lt;li&gt;If the path runs through &lt;code&gt;@typescript-eslint/types&lt;/code&gt;, upgrade to pnpm 12.0.0 today. If a Rust-CLI break blocks that upgrade, add an &lt;code&gt;overrides&lt;/code&gt; entry for &lt;code&gt;typescript&lt;/code&gt; at your 6.x version and revisit the upgrade this sprint.&lt;/li&gt;
&lt;li&gt;Grep your CI configs and Dockerfiles for &lt;code&gt;--legacy-peer-deps&lt;/code&gt;, &lt;code&gt;--force&lt;/code&gt;, and &lt;code&gt;strictPeerDependencies=false&lt;/code&gt;, and delete every hit that touches a TypeScript install. Each one converts an accurate refusal into a stack trace ten minutes later.&lt;/li&gt;
&lt;li&gt;Pin &lt;code&gt;packageManager&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; if it is not already, and add "pnpm version bump" to whatever your team treats as a dependency-review trigger.&lt;/li&gt;
&lt;li&gt;If you upgraded on purpose, keep 6.x as the source of truth for emit and tooling, and run TypeScript 7 as a second, non-blocking &lt;code&gt;tsc&lt;/code&gt; job until 7.1 lands on 2026-11-10.&lt;/li&gt;
&lt;li&gt;If lint takes more than a couple of minutes in CI, benchmark &lt;code&gt;tsgolint&lt;/code&gt; against your rule set this week and decide the two-rule gap knowingly rather than in an incident.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;What I read for this piece: the &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0" rel="noopener noreferrer"&gt;pnpm 12.0.0 release notes&lt;/a&gt;, typescript-eslint issues &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/3622" rel="noopener noreferrer"&gt;#3622&lt;/a&gt;, &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/10940" rel="noopener noreferrer"&gt;#10940&lt;/a&gt; and &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/12518" rel="noopener noreferrer"&gt;#12518&lt;/a&gt;, the &lt;a href="https://github.com/microsoft/TypeScript/issues/63703" rel="noopener noreferrer"&gt;TypeScript 7.1 iteration plan&lt;/a&gt;, the &lt;a href="https://oxc.rs/blog/2026-07-22-type-aware-linting-stable" rel="noopener noreferrer"&gt;oxc type-aware linting announcement&lt;/a&gt;, and Rush's &lt;a href="https://rushjs.io/pages/advanced/compatibility_db/" rel="noopener noreferrer"&gt;compatibility database&lt;/a&gt; page.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does typescript-eslint support TypeScript 7?&lt;/strong&gt;&lt;br&gt;
No. The support request, &lt;a href="https://github.com/typescript-eslint/typescript-eslint/issues/12518" rel="noopener noreferrer"&gt;#12518&lt;/a&gt;, was closed as not planned on GA day, because TypeScript 7.0 shipped without a stable programmatic API. The 7.1 iteration plan targets API stabilization for 2026-11-10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I stop pnpm from installing TypeScript 7 when my package.json pins 6.x?&lt;/strong&gt;&lt;br&gt;
Upgrade to pnpm 12.0.0, which removed the compatibility-database entry that gave &lt;code&gt;@typescript-eslint/types&lt;/code&gt; an unpinned &lt;code&gt;typescript&lt;/code&gt; dependency. Failing that, add an &lt;code&gt;overrides&lt;/code&gt; entry pinning &lt;code&gt;typescript&lt;/code&gt; to your 6.x line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;ignoreCompatibilityDb&lt;/code&gt; safe to turn on?&lt;/strong&gt;&lt;br&gt;
It resolves this specific break and disables every other bundled &lt;code&gt;packageExtensions&lt;/code&gt; fixup at the same time, including ones unrelated packages rely on. Use it as a temporary unblock, then upgrade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I lint with type information on TypeScript 7 today?&lt;/strong&gt;&lt;br&gt;
Yes, through oxc's &lt;code&gt;tsgolint&lt;/code&gt;, stable since 22 July 2026. It links typescript-go directly, tracks v7.0.2, and covers 59 of the 61 type-aware rules.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-eslint-cannot-read-properties-of-undefined-intrinsic.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>pnpm 12 Upgrade: Fix 12 Rust CLI Traps Before CI Breaks</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Thu, 27 Aug 2026 02:09:16 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/pnpm-12-upgrade-fix-12-rust-cli-traps-before-ci-breaks-k5p</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/pnpm-12-upgrade-fix-12-rust-cli-traps-before-ci-breaks-k5p</guid>
      <description>&lt;p&gt;pnpm 12.0.0 was published on 2026-08-26 with the Rust CLI as the real CLI. The happy path is uneventful: same commands, same flags, same &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; format. The traps live in the edges, and every one of them shipped less than eight weeks ago, which means any assistant answering from training data will describe pnpm 11 behaviour with total confidence. These are the twelve things I check before pnpm 12 goes near a CI runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; pin &lt;code&gt;packageManager&lt;/code&gt; before you upgrade anything. pnpm 12 finally validates &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; instead of dropping unknown keys in silence, but the check only fails hard when the project pins a pnpm version the running pnpm satisfies. With no pin, a typo in &lt;code&gt;minimumReleaseAge&lt;/code&gt; stays a warning and your dependency cooldown stays off.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A["Unknown key in pnpm-workspace.yaml"] --&amp;gt; B{"Project pins a pnpm version?"}
  B --&amp;gt;|"yes, pnpm 12 satisfies it"| C["ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS\ncommand fails"]
  B --&amp;gt;|"no pin"| D["Warning only\ninstall continues, key still ignored"]&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  What actually breaks when you upgrade to pnpm 12?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin the pnpm version before you touch anything else.&lt;/strong&gt; The v12.0.0 release notes give the new error as &lt;code&gt;ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS&lt;/code&gt;, and the RC 9 notes spell out the condition: the command fails when the project pins a pnpm version that the running pnpm satisfies. Everywhere else, the same unknown key prints a warning, the install continues, and the setting is still dropped. Put the pin in &lt;code&gt;package.json&lt;/code&gt; (or use &lt;code&gt;devEngines.packageManager&lt;/code&gt;) so the upgrade runs against a check with teeth:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"packageManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm@12.0.0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for a misspelled &lt;code&gt;minimumReleaseAge&lt;/code&gt; first.&lt;/strong&gt; This is the one that made me write the list. Under pnpm 11 a mistyped key in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; disappeared without a word, and &lt;code&gt;minimumReleaseAge&lt;/code&gt; is the key that holds your supply-chain cooldown: the delay that stops an install from pulling a package published an hour ago. A typo there means every install has been taking the newest thing on the registry while the config file said otherwise, which is exactly the exposure window that npm addressed with its &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;install-script allowlist&lt;/a&gt;. pnpm 12 suggests the closest real setting name when a key looks like a typo, so let it audit for you:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--frozen-lockfile&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'unrecognized|did you mean'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that on every repo before you bump a single dependency.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Move the SSH rewrite out of the repo and into git config.&lt;/strong&gt; Per pnpm's &lt;em&gt;What's different in pnpm 12&lt;/em&gt; post (2026-08-10), GitHub, GitLab and Bitbucket specifiers are identities now: &lt;code&gt;kevva/is-positive&lt;/code&gt;, &lt;code&gt;github:…&lt;/code&gt;, &lt;code&gt;git+https://…&lt;/code&gt; and &lt;code&gt;git+ssh://…&lt;/code&gt; all resolve to the same dependency, and pnpm never records an SSH URL for those hosts. Private repos that relied on the SSH form living in the lockfile fail to fetch on a runner with a deploy key. Push the rewrite down to git, where it applies to every fetch:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git config &lt;span class="nt"&gt;--global&lt;/span&gt; url.&lt;span class="s2"&gt;"ssh://git@github.com/"&lt;/span&gt;.insteadOf &lt;span class="s2"&gt;"https://github.com/"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth remembering what a git dependency is while you do this: upstream source you build locally, the same trust position that made the &lt;a href="///blog/arrayref-attack-shows-cargo-build-rs-runs-any-code.html"&gt;arrayref &lt;code&gt;build.rs&lt;/code&gt; compromise&lt;/a&gt; work.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;globalShims&lt;/code&gt; changes what &lt;code&gt;node&lt;/code&gt; means on a build agent.&lt;/strong&gt; pnpm 12 adds &lt;code&gt;globalShims&lt;/code&gt;, defaulting to &lt;code&gt;{ node: true, deno: true, bun: true }&lt;/code&gt;, so a globally installed &lt;code&gt;node&lt;/code&gt; honours the version the current project pins. That is excellent on a laptop and unwelcome on an agent that was built around one fixed toolchain and runs jobs from six repos. I set &lt;code&gt;globalShims: false&lt;/code&gt; in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; on runners and keep version selection in the CI image, because a global binary whose meaning depends on the working directory turns "which node ran this build" into archaeology.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;pnpm add -g yarn&lt;/code&gt; now installs current Yarn.&lt;/strong&gt; The same post confirms pnpm 12 installs the actual package-manager tools rather than npm wrappers: &lt;code&gt;pnpm add -g yarn&lt;/code&gt; lands on the current Yarn line, and &lt;code&gt;pnpm add -g node&lt;/code&gt; installs a real Node.js release instead of a wrapper that downloads one at first run. Any bootstrap script that has been quietly shipping Yarn 1 for years now ships Yarn 4. Read your provisioning scripts before the runners do.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;--resolution-only&lt;/code&gt; is gone, and the failure carries no pnpm error code.&lt;/strong&gt; &lt;code&gt;pnpm peers check&lt;/code&gt; replaces it. What the old flag gets you is a clap argument-parser message:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   error: unexpected argument '--resolution-only' found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in that line says &lt;code&gt;ERR_PNPM_&lt;/code&gt;, which is why it gets filed as a shell quoting problem. Treat any bare &lt;code&gt;error: unexpected argument&lt;/code&gt; from pnpm as "the Rust CLI dropped this flag" and go find the replacement command. It needs the same reading discipline as the &lt;a href="///blog/npm-approve-scripts-vs---allow-scripts-pick-the-right-flag.html"&gt;npm approve-scripts and --allow-scripts split&lt;/a&gt;: the flag name that used to work tells you nothing about the one that works now.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit &lt;code&gt;.pnpmfile.cjs&lt;/code&gt; for &lt;code&gt;filterLog&lt;/code&gt;.&lt;/strong&gt; The v12.0.0 notes deprecate the pnpmfile &lt;code&gt;filterLog&lt;/code&gt; hook: the Rust CLI ignores it and emits a warning. Teams used it to scrub tokens and cut noise out of install logs, and that scrubbing stops the moment you upgrade, with a single warning line as the only marker. Check the file, then check what your CI log retention has been capturing since the upgrade landed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reject &lt;code&gt;..&lt;/code&gt; in workspace globs before pnpm does.&lt;/strong&gt; Issue &lt;a href="https://github.com/pnpm/pnpm/issues/13880" rel="noopener noreferrer"&gt;#13880&lt;/a&gt;, opened 2026-08-12, reports parent-relative workspace packages breaking on 12: pnpm omits them from discovery, then refuses the lockfile importer with &lt;code&gt;Refusing to install importer with unsafe path key '../shared'&lt;/code&gt;. The message goes on to require POSIX paths relative to the workspace root, rejecting absolute paths, drive prefixes and &lt;code&gt;..&lt;/code&gt; components. One grep tells you whether you are exposed:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'\.\.'&lt;/span&gt; pnpm-workspace.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Dry-run &lt;code&gt;engineStrict&lt;/code&gt; against your optional subtrees.&lt;/strong&gt; Under &lt;code&gt;engineStrict&lt;/code&gt;, pnpm 12 fails when an incompatible package is reached through a regular &lt;code&gt;dependencies&lt;/code&gt; edge, even where that edge hangs below an &lt;code&gt;optionalDependencies&lt;/code&gt; subtree. Packages tolerated for years because an optional parent covered for them now block the install outright. Flip &lt;code&gt;engineStrict&lt;/code&gt; on locally and run a full install before you change the version in CI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Land the lockfile churn as its own commit.&lt;/strong&gt; pnpm 12 breaks dependency cycles in a canonical order, which buys deterministic, smaller lockfiles and 2 to 3 times faster peer resolution, and it re-keys the affected peer variants on the first resolution pass after the upgrade. Run &lt;code&gt;pnpm install&lt;/code&gt;, commit &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; alone, and keep that diff away from any dependency bump. A reviewer can only spot a real change when the mechanical churn shipped separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check &lt;code&gt;readlink&lt;/code&gt; on the deploy output, because the build stays green either way.&lt;/strong&gt; Issue &lt;a href="https://github.com/pnpm/pnpm/issues/13618" rel="noopener noreferrer"&gt;#13618&lt;/a&gt; documents a regression introduced in 11.19.0 and still labelled &lt;em&gt;state: needs design&lt;/em&gt;: &lt;code&gt;pnpm deploy --legacy&lt;/code&gt; leaves a workspace dependency that carries &lt;code&gt;peerDependencies&lt;/code&gt; as a symlink escaping the deploy directory. CI passes, because the target still exists on the runner, and the container dies at startup with &lt;code&gt;ERR_MODULE_NOT_FOUND&lt;/code&gt;. One line in the pipeline catches it:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;readlink &lt;/span&gt;deploy/node_modules/shared | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'^\.\./\.\./'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Workspace deps without peers still pack correctly, which is why the small reproduction someone builds to check this comes back clean.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Drop the overrides you added for the ESLint &lt;code&gt;Intrinsic&lt;/code&gt; crash.&lt;/strong&gt; RC 10 (2026-08-24) removed statically-detected entries from the built-in compatibility database. Those entries had given &lt;code&gt;@typescript-eslint/types&lt;/code&gt; a &lt;code&gt;typescript&lt;/code&gt; dependency that resolved to the newest release, putting TypeScript 7 under older &lt;code&gt;@typescript-eslint&lt;/code&gt; versions and making ESLint fail with &lt;code&gt;Cannot read properties of undefined (reading 'Intrinsic')&lt;/code&gt;. The database keeps its &lt;code&gt;@yarnpkg/extensions&lt;/code&gt; and pnpm-curated entries. Any pin you added to work around that crash is dead weight in the manifest, and dead pins outlive the people who remember why they exist.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Pin the pnpm version before you upgrade, then read the warnings the pinned run produces. pnpm 12 has genuinely good diagnostics: typo suggestions, an unrecognized-settings error, a strict importer check. Almost all of them degrade to a warning, or to nothing at all, when the project gives them no pin to check against. That is the same failure mode as an &lt;a href="///blog/fix-npm-audit-410-and-invalid-json-response-body-bugs.html"&gt;npm audit that returns 410 and passes anyway&lt;/a&gt;: the tool tells you the truth only once you have configured it to be able to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does pnpm 12 change the lockfile format?&lt;/strong&gt; No. The format is unchanged from pnpm 11. The content changes, because cycles are now broken in a canonical order and the affected peer variants get re-keyed on the first resolution pass, so expect one large diff and commit it alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why did my unknown &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; key only warn?&lt;/strong&gt; The project has no pnpm version pin. &lt;code&gt;ERR_PNPM_UNRECOGNIZED_WORKSPACE_SETTINGS&lt;/code&gt; fails the command only when the project pins a version the running pnpm satisfies; otherwise you get a warning and the key is still ignored.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What replaced &lt;code&gt;pnpm install --resolution-only&lt;/code&gt;?&lt;/strong&gt; &lt;code&gt;pnpm peers check&lt;/code&gt;. The removed flag surfaces as &lt;code&gt;error: unexpected argument '--resolution-only' found&lt;/code&gt;, straight from the argument parser, with no &lt;code&gt;ERR_PNPM_&lt;/code&gt; code attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;pnpm deploy&lt;/code&gt; safe on 12?&lt;/strong&gt; &lt;code&gt;pnpm deploy --legacy&lt;/code&gt; still carries issue #13618, open since 11.19.0 and labelled needs design. Add the &lt;code&gt;readlink&lt;/code&gt; check from tip 11 to the pipeline rather than waiting for the fix.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What I read: the &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0" rel="noopener noreferrer"&gt;v12.0.0 release notes&lt;/a&gt; and &lt;a href="https://github.com/pnpm/pnpm/releases/tag/v12.0.0-rc.10" rel="noopener noreferrer"&gt;RC 10 notes&lt;/a&gt; on GitHub, pnpm's own &lt;a href="https://pnpm.io/blog/whats-different-in-pnpm-12" rel="noopener noreferrer"&gt;What's different in pnpm 12&lt;/a&gt;, and issues &lt;a href="https://github.com/pnpm/pnpm/issues/13618" rel="noopener noreferrer"&gt;#13618&lt;/a&gt; and &lt;a href="https://github.com/pnpm/pnpm/issues/13880" rel="noopener noreferrer"&gt;#13880&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/pnpm-12-upgrade-fix-12-rust-cli-traps-before-ci-breaks.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tips</category>
    </item>
    <item>
      <title>Stop Trusting C2PA Content Credentials on Android</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Wed, 26 Aug 2026 02:09:25 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/stop-trusting-c2pa-content-credentials-on-android-4nac</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/stop-trusting-c2pa-content-credentials-on-android-4nac</guid>
      <description>&lt;p&gt;On 25 August 2026, David Buchanan published an AI-generated image carrying a valid C2PA Content Credential that says a Google Pixel camera captured it, &lt;a href="https://www.da.vidbuchanan.co.uk/blog/android-c2pa.html" rel="noopener noreferrer"&gt;along with a YouTube clip signed the same way&lt;/a&gt;. The official C2PA verification tool accepts both. The signing key was never extracted from the phone, and that detail is the reason this is hard to fix.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A Content Credential proves which key signed some bytes. It does not prove a sensor saw anything. On a Pixel rooted by a software exploit, StrongBox will sign attacker-supplied content with the real camera key, and the attestation certificate still reports a locked bootloader. Because Pixel issues one certificate per image, there is nothing to revoke. Score verified credentials as weak corroboration with a device-class caveat, and score a missing credential as nothing at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What does a valid Content Credential actually prove?
&lt;/h2&gt;

&lt;p&gt;The C2PA binding runs from the manifest to the asset bytes, and from the signature to a certificate that chains to a trust list. At no point does anything in that chain observe the image sensor. The guarantee on offer is "a key on the trust list signed these bytes," and everything else people read into a green check is inference about how well that key is guarded.&lt;/p&gt;

&lt;p&gt;On Pixel, the guard is real hardware. Google's &lt;a href="https://blog.google/security/pixel-android-trusted-images-c2pa-content-credentials/" rel="noopener noreferrer"&gt;September 2025 announcement&lt;/a&gt; states that Pixel Camera's "C2PA claim signing keys are generated and stored using Android StrongBox in the Titan M2 security chip," and that Pixel Camera reached Assurance Level 2, "the highest security rating currently defined by the C2PA Conformance Program," which Google notes is "currently only possible on the Android platform."&lt;/p&gt;

&lt;p&gt;Buchanan's result leaves that hardware claim intact. Root on the device can call the KeyStore API while impersonating the camera app, hand StrongBox arbitrary bytes, and get a signature over them. His tool, &lt;code&gt;Keystork&lt;/code&gt;, does exactly that. He states every C2PA camera app relying on &lt;code&gt;Android_KeyAttestation&lt;/code&gt; or &lt;code&gt;Google_PlayIntegrity&lt;/code&gt; is affected, which covers the Android field as it exists today.&lt;/p&gt;

&lt;p&gt;The attestation certificate that ships alongside is the part that fools operators. It reports genuine hardware, a locked bootloader, unmodified AVB keys, and Play Protect Certified boot. Every one of those statements is still true on a phone rooted through a kernel bug. Attestation measures the boot chain. Root acquired after boot is invisible to it. Anyone who has argued about what &lt;a href="///blog/a2a-agent-card-poisoning-signed-but-lying.html"&gt;a JWS on an agent card really certifies&lt;/a&gt; has met this gap before: the signature is honest about origin and silent about everything the operator cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  How expensive is the root that makes this work?
&lt;/h2&gt;

&lt;p&gt;Cheap enough to stop treating as a nation-state problem. Buchanan attributes in-the-wild one-click root on fully patched Pixels to &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-43499" rel="noopener noreferrer"&gt;CVE-2026-43499&lt;/a&gt;, a Linux kernel &lt;code&gt;rtmutex&lt;/code&gt; use-after-free (CWE-416) published to NVD on 21 May 2026 with a CVSS base score of 7.8, fixed in 6.1.175, 6.6.140, 6.12.86, 6.18.27 and 7.0.4. He reports testing on Pixel 8a and 9a at August patch levels. He also describes a hardware fault-injection path, flipping DRAM bits into page table entries, which no kernel patch closes.&lt;/p&gt;

&lt;p&gt;Google's response was to close the report "Won't fix (infeasible)" and pay a $7,500 out-of-scope bounty. Read that as a statement about the threat model rather than a shrug: Android key attestation was designed to prove device integrity at boot for DRM and payments, and it does that job. C2PA asked it to prove something it was never built to prove, and Google is declining to redefine it.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
  A["Sensor capture"] --&amp;gt; B["Camera app builds\nC2PA claim"]
  B --&amp;gt; C["StrongBox / Titan M2\nsigns the claim"]
  C --&amp;gt; D["Attestation cert:\nlocked bootloader,\nAVB keys, patch level"]
  D --&amp;gt; E{"Validator:\nchains to trust list?"}
  E --&amp;gt;|yes| F["Verified as\ncamera-captured"]
  G["Root via CVE-2026-43499\nor DRAM fault injection"] --&amp;gt; H["Attacker requests\na signature over\narbitrary bytes"]
  H --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Four different states, one identical verifier output
&lt;/h2&gt;

&lt;p&gt;Most of the confusion in the last week comes from teams collapsing distinct situations into a single boolean.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the verifier shows&lt;/th&gt;
&lt;th&gt;What actually happened&lt;/th&gt;
&lt;th&gt;The check that separates it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valid, trusted signer&lt;/td&gt;
&lt;td&gt;Genuine capture&lt;/td&gt;
&lt;td&gt;Nothing available today distinguishes this from row 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid, trusted signer&lt;/td&gt;
&lt;td&gt;Forgery on a software-rooted device&lt;/td&gt;
&lt;td&gt;Device class and attestation method in the manifest; treat Android AL2 as weak evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valid, trusted signer&lt;/td&gt;
&lt;td&gt;Certificate known-compromised and revoked&lt;/td&gt;
&lt;td&gt;A manual revocation check; conforming validators skip it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No credential&lt;/td&gt;
&lt;td&gt;Screenshot, CDN resize, re-encode, or a deliberate strip&lt;/td&gt;
&lt;td&gt;Absence carries no signal in either direction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row four matters more than it looks. Stripping happens by accident on almost every resize and screenshot, and purpose-built C2PA strippers have been easy to find on GitHub this month. An intake policy that raises a risk score for missing provenance will spend its life flagging holiday photos that went through an image CDN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why revocation cannot save the Pixel design
&lt;/h2&gt;

&lt;p&gt;Google describes a "One-and-Done" strategy where each key and certificate is used to sign exactly one image. That is excellent hygiene against key reuse and it removes the object you would revoke. There is no device certificate. Each forgery arrives with a fresh certificate, freshly attested. The remaining lever is refusing to provision new certificates to a compromised device, which requires detecting software root, which is precisely the thing attestation cannot see.&lt;/p&gt;

&lt;p&gt;Nikon had the opposite design and used it. After Adam Horshack demonstrated the Z6 III's multiple-exposure mode signing an image the camera never captured, Nikon suspended its Authenticity Service and, per DPReview and Nikon Rumors, revoked every certificate issued since the 21 September 2025 launch. That invalidated legitimate photographs along with the forgeries. Those are the two shapes available in 2026: revoke nothing, or revoke everyone.&lt;/p&gt;

&lt;p&gt;The academic picture is worse. The first formal-methods analysis of C2PA, by Golaszewski, Krawetz, Sherman and colleagues (&lt;a href="https://eprint.iacr.org/2026/804" rel="noopener noreferrer"&gt;IACR ePrint 2026/804&lt;/a&gt;, revised 2 June 2026), reports that "inadequate certificate revocation policies cause conforming validators to accept manifests signed with known-compromised certificates, violating all security goals." The same paper finds that generators and validators disagree on the trusted timestamp, that conforming validators return inconsistent results on the same asset, that the exclusion range permits undetectable alterations, and that "the conformance program certifies products without technical review or defined requirements." The authors note some fixes landed in Pixel 10 Pro and C2PA v2.3, so the specification is moving. The deployed fleet is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timestamp is right for builds and wrong for cameras
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html" rel="noopener noreferrer"&gt;C2PA 2.2 specification&lt;/a&gt; (2025-05-01) directs generators to obtain an RFC 3161 countersignature so a manifest keeps verifying after its certificate expires or is revoked. For build provenance that is the correct default, and it is why a two-year-old release artifact still validates. Anyone who has fought through &lt;a href="///blog/cosign-v3-sign-and-verify-images-fix-harbor-breaks.html"&gt;a Cosign v3 rollout and the trust-chain breakage that came with it&lt;/a&gt; knows how much that durability is worth.&lt;/p&gt;

&lt;p&gt;Capture provenance wants the opposite behaviour. When a camera is known compromised, its output should stop verifying immediately, including images signed last year. C2PA inherited the build-provenance default and applied it to real-world claims, and the countersignature now works to keep forgeries alive. Decide which of those two claims your own signatures are making before you copy the pattern into anything else you sign.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your verification gate is probably trusting nothing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;c2patool -d asset.jpg&lt;/code&gt; prints the detailed manifest and certificate chain. Trust anchors are supplied through &lt;code&gt;C2PATOOL_TRUST_ANCHORS&lt;/code&gt;, &lt;code&gt;C2PATOOL_ALLOWED_LIST&lt;/code&gt; and &lt;code&gt;C2PATOOL_TRUST_CONFIG&lt;/code&gt;, per the &lt;a href="https://github.com/contentauth/c2pa-rs/blob/main/cli/docs/usage.md" rel="noopener noreferrer"&gt;c2pa-rs CLI usage docs&lt;/a&gt;, and contentcredentials.org publishes &lt;code&gt;anchors.pem&lt;/code&gt; and &lt;code&gt;allowed.sha256.txt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failure mode to hunt for is the one that reports success while doing nothing. In a settings file, &lt;code&gt;trust.trust_anchors&lt;/code&gt; takes PEM text rather than a path. Hand it a filename and the tool configures no anchors at all, then happily reports valid on assets that chain to nowhere. There is also a dead-list trap: the C2PA Interim Trust List was frozen as of 1 January 2026, leaving the Conformance Programme as the only issuance path, so a gate still pinning the ITL is validating against a list that stopped growing eight months ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change in your intake pipeline this week
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Rewrite any rule that reads &lt;code&gt;verified == true → accept&lt;/code&gt;. For the Android fleet, that rule is now wrong. Downgrade a passing Content Credential to weak corroborating evidence carrying a device-class caveat, and require a second, independent signal before an image drives a payout, a publication, or a KYC decision.&lt;/li&gt;
&lt;li&gt;Delete any risk score attached to a missing manifest. Absence proves a resize happened, nothing more.&lt;/li&gt;
&lt;li&gt;Prove your gate pins an anchor. Put a known-untrusted signed asset in CI and require the pipeline to fail on it. If it passes, your &lt;code&gt;trust.trust_anchors&lt;/code&gt; is holding a path instead of PEM text, and you have been rubber-stamping everything.&lt;/li&gt;
&lt;li&gt;Re-pull &lt;code&gt;anchors.pem&lt;/code&gt; and &lt;code&gt;allowed.sha256.txt&lt;/code&gt; from contentcredentials.org and confirm nothing in your config still references the Interim Trust List frozen on 1 January 2026.&lt;/li&gt;
&lt;li&gt;Record the signer identity and attestation method (&lt;code&gt;Android_KeyAttestation&lt;/code&gt;, &lt;code&gt;Google_PlayIntegrity&lt;/code&gt;, or a hardware vendor's own) from every manifest at intake, alongside the asset hash. When the next device class breaks, that field is how you find the affected corpus. The same discipline that makes &lt;a href="///blog/generate-an-sbom-with-syft-filter-grype-with-vex.html"&gt;a stored SBOM plus VEX&lt;/a&gt; useful the day a CVE lands applies here, and neither can be reconstructed after the fact.&lt;/li&gt;
&lt;li&gt;Flag Pixel 8a and 9a class devices at August 2026 patch levels specifically, since those are the models Buchanan reports testing, and keep the flag until you can point at a device generation where the ePrint 2026/804 fixes shipped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I read for this: &lt;a href="https://www.da.vidbuchanan.co.uk/blog/android-c2pa.html" rel="noopener noreferrer"&gt;Buchanan's write-up&lt;/a&gt; for the mechanism, &lt;a href="https://eprint.iacr.org/2026/804" rel="noopener noreferrer"&gt;ePrint 2026/804&lt;/a&gt; for the formal analysis of revocation and validator disagreement, &lt;a href="https://blog.google/security/pixel-android-trusted-images-c2pa-content-credentials/" rel="noopener noreferrer"&gt;Google's Pixel C2PA announcement&lt;/a&gt; for the StrongBox and Assurance Level 2 claims, &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-43499" rel="noopener noreferrer"&gt;NVD's entry for CVE-2026-43499&lt;/a&gt; for the kernel bug and fixed versions, and the &lt;a href="https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html" rel="noopener noreferrer"&gt;C2PA 2.2 specification&lt;/a&gt; for the RFC 3161 countersignature requirement.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/stop-trusting-c2pa-content-credentials-on-android.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>Stop Kubernetes DRA Giving One GPU to a VM and a Pod</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Tue, 25 Aug 2026 03:49:07 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/stop-kubernetes-dra-giving-one-gpu-to-a-vm-and-a-pod-40ch</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/stop-kubernetes-dra-giving-one-gpu-to-a-vm-and-a-pod-40ch</guid>
      <description>&lt;p&gt;NVIDIA GPU Operator 26.7.0 shipped on 21 August with the donated DRA driver as a managed component, and buried in the release notes is a sentence that should stop a platform team mid-rollout: with passthrough enabled, one physical GPU is published to the scheduler twice, and kube-scheduler can grant both claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; turn on DRA VFIO passthrough and every eligible GPU is advertised as a container device under &lt;code&gt;gpu.nvidia.com&lt;/code&gt; and as a passthrough device under &lt;code&gt;vfio.gpu.nvidia.com&lt;/code&gt;. The driver only notices the overlap during Prepare, which runs after the scheduler has committed, so a container claim and a VM claim for the same card can both succeed. NVIDIA suggests submitting workloads sequentially. Dedicate the nodes instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 26.7.0 release notes actually admit
&lt;/h2&gt;

&lt;p&gt;The wording in the &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/26.7/release-notes.html" rel="noopener noreferrer"&gt;GPU Operator 26.7.0 release notes&lt;/a&gt; is unusually direct: "With DRA passthrough enabled, the driver initially advertises each eligible physical GPU as both a container GPU and a VFIO device. If a container GPU claim and a VFIO passthrough claim for the same physical GPU are allocated before either device is prepared, the scheduler can allocate both claims."&lt;/p&gt;

&lt;p&gt;That paragraph is the complete public documentation of the problem. It describes the symptom and stops. The mechanism underneath it lives in a scheduling KEP that changed shape between Kubernetes 1.35 and 1.36, which is why an assistant asked about this today will answer from the device-plugin era, when a GPU had exactly one owner because there was exactly one &lt;code&gt;nvidia.com/gpu&lt;/code&gt; integer to decrement.&lt;/p&gt;

&lt;p&gt;Context for how fast this spreads: NVIDIA donated the GPU DRA driver to the Kubernetes community &lt;a href="https://blogs.nvidia.com/blog/nvidia-at-kubecon-2026/" rel="noopener noreferrer"&gt;at KubeCon Europe 2026 in Amsterdam&lt;/a&gt;, and it now lives at &lt;code&gt;kubernetes-sigs/dra-driver-nvidia-gpu&lt;/code&gt;. Release 26.7.0 promotes it to a first-class operand next to driver 595.91.07 and container toolkit v1.20.0. Teams that were waiting for DRA to feel supported are about to enable it. This is the same pattern as the &lt;a href="///blog/fix-a-mirror-pod-may-not-reference-secrets-in-k8s-1-37.html"&gt;mirror pod secret rejection in 1.37&lt;/a&gt;: a node-level component is the only thing that knows the truth, and it learns it too late to say no.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why kube-scheduler cannot catch this
&lt;/h2&gt;

&lt;p&gt;DRA splits a device lifecycle into three stages. The kubelet plugin publishes ResourceSlices describing what exists on the node. kube-scheduler allocates claims against those slices, cluster-wide, with no node-local knowledge beyond what was published. Then &lt;code&gt;NodePrepareResources&lt;/code&gt; runs on the node and produces the CDI spec that actually wires the device into the workload.&lt;/p&gt;

&lt;p&gt;Physical exclusivity is only knowable at stage three. The binding decision happened at stage two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["Physical GPU 0\nnode gpu-a1"] --&amp;gt; B["ResourceSlice\ndriver gpu.nvidia.com"]
  A --&amp;gt; C["ResourceSlice\ndriver vfio.gpu.nvidia.com"]
  B --&amp;gt; D{"kube-scheduler\nallocates from slices"}
  C --&amp;gt; D
  D --&amp;gt;|"container claim"| E["Prepare\nCDI spec for /dev/nvidia0"]
  D --&amp;gt;|"VFIO claim"| F["Prepare\nbind vfio-pci, /dev/vfio/devices/vfio0"]
  E --&amp;gt; G["One GPU, two owners"]
  F --&amp;gt; G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two claims, two Prepare calls, one card. The container path opens &lt;code&gt;/dev/nvidia0&lt;/code&gt;. The VFIO path unbinds the &lt;code&gt;nvidia&lt;/code&gt; kernel driver from the PCI function and binds &lt;code&gt;vfio-pci&lt;/code&gt;. If a training job already holds the character device open, that unbind either returns EBUSY or tears the device out from under a running process.&lt;/p&gt;

&lt;p&gt;The asymmetry is the part I would plan around. The container job dies loudly and someone files a ticket. The VM boots against a card whose teardown path never completed. Whether framebuffer contents survive that exact sequence is a test I would run on a spare node before trusting a tenancy boundary to it, and I have found no published result either way. If you sell GPU capacity to more than one tenant, that is now a boundary you own, in the same family of questions as &lt;a href="///blog/kubernetes-user-namespaces-in-1-36-with-hostusers-false.html"&gt;running workloads under user namespaces in 1.36&lt;/a&gt; and &lt;a href="///blog/kubernetes-default-deny-egress-stops-pod-exfiltration.html"&gt;default-deny egress between tenants&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing the VFIO DeviceClass is not evidence you are exposed
&lt;/h2&gt;

&lt;p&gt;Here is where I expect the panic to be wrong. The &lt;code&gt;PassthroughSupport&lt;/code&gt; feature gate is Alpha and off by default in the DRA driver, and that single fact is what keeps this from being a fleet-wide incident this week.&lt;/p&gt;

&lt;p&gt;NVIDIA's &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/dra-intro-install.html" rel="noopener noreferrer"&gt;DRA install documentation&lt;/a&gt; notes that the passthrough capability "is always enabled, although VFIO passthrough is unusable without the Alpha &lt;code&gt;PassthroughSupport&lt;/code&gt; feature gate." So &lt;code&gt;vfio.gpu.nvidia.com&lt;/code&gt; shows up in &lt;code&gt;kubectl get deviceclasses&lt;/code&gt; on clusters that have no passthrough at all. Someone on your team will screenshot that list and call it a finding. Check the gate on the kubelet plugin before you page anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your 2023 pod manifest may already be a DRA claim
&lt;/h2&gt;

&lt;p&gt;The second-order effect is nastier than the headline, and it has nothing to do with VMs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DRAExtendedResource&lt;/code&gt; (KEP-5004) is enabled by default from Kubernetes v1.36.0. It makes the scheduler translate a plain &lt;code&gt;resources.limits: nvidia.com/gpu: 1&lt;/code&gt; into an auto-created ResourceClaim against whichever DeviceClass carries &lt;code&gt;spec.extendedResourceName: nvidia.com/gpu&lt;/code&gt;. A manifest written three years ago and never touched since is now flowing through the DRA allocation path. Nobody opted in, and nothing in the pod spec changed.&lt;/p&gt;

&lt;p&gt;That translation has already produced a leak of its own. &lt;a href="https://github.com/kubernetes/kubernetes/issues/134311" rel="noopener noreferrer"&gt;kubernetes/kubernetes#134311&lt;/a&gt;, filed against v1.34.0 with &lt;code&gt;DRAExtendedResource: true&lt;/code&gt;, reported scheduler-created claims still sitting in &lt;code&gt;allocated,reserved&lt;/code&gt; after their pods had completed. It is closed and fixed, and it marks where the sharp edges are on this path: claims whose lifecycle is owned by the scheduler rather than by you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kubernetes already has the primitive that would close this
&lt;/h2&gt;

&lt;p&gt;KEP-4815 Partitionable Devices exists for exactly this shape of problem. It adds &lt;code&gt;sharedCounters&lt;/code&gt; and &lt;code&gt;consumesCounters&lt;/code&gt; to ResourceSlice so a driver can model overlapping views of one piece of hardware: allocate one device, and the counters it consumes stop being available to its siblings. That is how MIG profiles express that two partitions want the same memory slice.&lt;/p&gt;

&lt;p&gt;Per the &lt;a href="https://github.com/kubernetes/enhancements/blob/master/keps/sig-scheduling/4815-dra-partitionable-devices/README.md" rel="noopener noreferrer"&gt;KEP README&lt;/a&gt;, &lt;code&gt;DRAPartitionableDevices&lt;/code&gt; graduates to beta in 1.36, and a backwards-incompatible revision between 1.34 and 1.35 now requires &lt;code&gt;sharedCounters&lt;/code&gt; to live in a separate ResourceSlice from the devices consuming them, in the same pool with matching generation. A driver migrating across that break has a defensible reason to ship the overlap unmodeled and write a known-issue paragraph. It still leaves operators holding the risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find double-advertised GPUs in your own cluster
&lt;/h2&gt;

&lt;p&gt;The condition is invisible in &lt;code&gt;kubectl describe node&lt;/code&gt;. Node capacity looks normal because neither driver is reporting an integer count any more. What you are hunting is one GPU UUID appearing under two driver names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get resourceslices &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  .items[] | .spec.driver as $d | .spec.nodeName as $n |
  .spec.devices[]? | [$n, $d, .name, (.attributes.uuid.string // "-")] | @tsv'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-k4&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-f3&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any UUID on two rows with different drivers is double-advertised. Then confirm the gate rather than inferring it from the DeviceClass list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get deviceclass gpu.nvidia.com &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.spec.extendedResourceName}{"\n"}'&lt;/span&gt;
kubectl &lt;span class="nt"&gt;-n&lt;/span&gt; gpu-operator get pods &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;nvidia-dra-driver-gpu-kubelet-plugin &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.items[0].spec.containers[0].args}'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; feature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command tells you whether legacy &lt;code&gt;nvidia.com/gpu&lt;/code&gt; manifests are being translated into claims. The second tells you whether passthrough is genuinely live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you turn on PassthroughSupport
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Split the node pool first, at the provisioner level.&lt;/strong&gt; Label passthrough nodes and taint them (&lt;code&gt;nvidia.com/gpu.workload=vfio:NoSchedule&lt;/code&gt;), then make the container DeviceClass selector exclude them. Encode it in the Karpenter NodePool or node-group config so it survives autoscaling. Typed GPU pools strand capacity, and on a card this expensive that hurts. Take the strand until &lt;code&gt;DRAPartitionableDevices&lt;/code&gt; closes the gap at the scheduler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reject sequential submission as a control.&lt;/strong&gt; NVIDIA's first workaround asks you to hand-serialize a system built to avoid serializing. It survives a demo. It does not survive one Argo burst or one Karpenter node coming online with two pending pods.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the ResourceSlice UUID query on every cluster already on 26.7.0.&lt;/strong&gt; A collision means a live allocation race, today, with no alert attached to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit leaked claims if you are on Kubernetes 1.36 or later.&lt;/strong&gt; List ResourceClaims whose &lt;code&gt;ownerReferences&lt;/code&gt; point at a Pod in &lt;code&gt;Succeeded&lt;/code&gt; or &lt;code&gt;Failed&lt;/code&gt; while status is still allocated. Those are GPUs you are billed for and cannot schedule, per the failure mode in issue #134311.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin the stack explicitly.&lt;/strong&gt; GPU Operator 26.7.0, driver 595.91.07, container toolkit v1.20.0, on Kubernetes v1.34.2 or later, which the DRA driver requires for the &lt;code&gt;resource.k8s.io&lt;/code&gt; DeviceClass API. Version drift here changes which of the above applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a watch on &lt;code&gt;DRAPartitionableDevices&lt;/code&gt; reaching beta in your distro.&lt;/strong&gt; Once the NVIDIA driver models the container and VFIO views as counter-consuming siblings in one pool, the scheduler enforces exclusivity and the typed node pools become an optimization rather than a safety mechanism.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does GPU Operator 26.7.0 double-allocate GPUs by default?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;PassthroughSupport&lt;/code&gt; is Alpha and off by default, so the VFIO view of the card is unusable until you enable the gate. The DeviceClass is registered either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I tell if a GPU is advertised twice?&lt;/strong&gt;&lt;br&gt;
Group ResourceSlice devices by their &lt;code&gt;uuid&lt;/code&gt; attribute across driver names. A UUID under both &lt;code&gt;gpu.nvidia.com&lt;/code&gt; and &lt;code&gt;vfio.gpu.nvidia.com&lt;/code&gt; on one node is double-advertised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why can kube-scheduler not prevent this?&lt;/strong&gt;&lt;br&gt;
It allocates at stage two of publish, allocate, prepare. The driver reconciles the two views during &lt;code&gt;NodePrepareResources&lt;/code&gt; at stage three, after the binding decision is already made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does an old manifest using &lt;code&gt;nvidia.com/gpu&lt;/code&gt; limits go through DRA now?&lt;/strong&gt;&lt;br&gt;
On Kubernetes 1.36 and later it can. &lt;code&gt;DRAExtendedResource&lt;/code&gt; is on by default from v1.36.0 and auto-creates a ResourceClaim against the DeviceClass carrying &lt;code&gt;spec.extendedResourceName: nvidia.com/gpu&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks first if both claims land?&lt;/strong&gt;&lt;br&gt;
The container job, when the &lt;code&gt;nvidia&lt;/code&gt; driver unbind hits an open &lt;code&gt;/dev/nvidia0&lt;/code&gt; and returns EBUSY or pulls the device away mid-run. The VM starts against a card whose teardown never finished cleanly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/stop-kubernetes-dra-giving-one-gpu-to-a-vm-and-a-pod.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
    </item>
    <item>
      <title>Fix "a mirror pod may not reference secrets" in K8s 1.37</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Tue, 25 Aug 2026 02:09:53 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-a-mirror-pod-may-not-reference-secrets-in-k8s-137-2165</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-a-mirror-pod-may-not-reference-secrets-in-k8s-137-2165</guid>
      <description>&lt;p&gt;Kubernetes v1.37 goes GA tomorrow, Wednesday 26 August 2026, per the &lt;a href="https://www.kubernetes.dev/resources/release/" rel="noopener noreferrer"&gt;release team's schedule&lt;/a&gt;. Under the ipvs deprecation and the containerd 2.0 floor sits a two-line removal that closes a four-year-old reporting hole: the &lt;code&gt;PreventStaticPodAPIReferences&lt;/code&gt; feature gate is gone, merged as &lt;a href="https://github.com/kubernetes/kubernetes/pull/140226" rel="noopener noreferrer"&gt;PR #140226&lt;/a&gt; on 6 July 2026. Short version: a static pod that references a Secret, ConfigMap, ServiceAccount, or PVC is now refused by the kubelet with no opt-out, and any node still pinning that gate to &lt;code&gt;false&lt;/code&gt; will fail to start on 1.37. Two greps on one node per pool tell you whether you are affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened when a static pod referenced a Secret?
&lt;/h2&gt;

&lt;p&gt;The rule itself is documented and boring. &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/static-pods/" rel="noopener noreferrer"&gt;The Static Pods docs&lt;/a&gt; state that "the spec of a static Pod cannot refer to other API objects, such as ServiceAccount, ConfigMap, or Secret." What almost nobody has looked at is the behavior when you ignored it, because the failure was split across two subsystems that never compared notes. The container started. The Pod object did not exist. Both were true at once, and only one of them was visible from &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Mike Spreitzer filed &lt;a href="https://github.com/kubernetes/kubernetes/issues/103587" rel="noopener noreferrer"&gt;issue #103587&lt;/a&gt; on 8 July 2021 with the log lines that prove it. The kubelet writes &lt;code&gt;"Creating a mirror pod for static pod" pod="default/test5-init1"&lt;/code&gt;, then immediately &lt;code&gt;"Failed creating a mirror pod for" err="pods \"test5-init1\" is forbidden: a mirror pod may not reference service accounts"&lt;/code&gt;. The workload keeps serving traffic the entire time. That asymmetry survived until v1.34 put a fix behind a gate, and v1.37 welds the gate shut.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["/etc/kubernetes/manifests/*.yaml"] --&amp;gt; B["kubelet static pod source"]
  B --&amp;gt; C["container starts via CRI"]
  B --&amp;gt; D["kubelet POSTs a mirror pod"]
  D --&amp;gt; E{"spec references an API object?"}
  E --&amp;gt;|no| F["Pod visible to kubectl,\npolicy engines, audits"]
  E --&amp;gt;|yes| G["NodeRestriction returns 403"]
  G --&amp;gt; H["No Pod object exists.\nContainer keeps running."]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 403 comes from the NodeRestriction admission plugin. In current master, &lt;code&gt;plugin/pkg/admission/noderestriction/admission.go&lt;/code&gt; formats the message generically as &lt;code&gt;"node %q can not create pods that reference %s"&lt;/code&gt;, alongside &lt;code&gt;"error checking mirror pod for API references: %w"&lt;/code&gt;. That &lt;code&gt;%s&lt;/code&gt; is filled by &lt;code&gt;podutil.HasAPIObjectReference()&lt;/code&gt; in &lt;code&gt;pkg/api/pod/util.go&lt;/code&gt;, which walks the spec and returns the plural resource name it found: &lt;code&gt;serviceaccounts&lt;/code&gt;, &lt;code&gt;secrets&lt;/code&gt;, &lt;code&gt;configmaps&lt;/code&gt;, &lt;code&gt;resourceclaims&lt;/code&gt;, &lt;code&gt;persistentvolumeclaims&lt;/code&gt;, plus qualified variants like &lt;code&gt;secrets (via azureFile volumes)&lt;/code&gt;, &lt;code&gt;csidrivers (via CSI volumes)&lt;/code&gt;, and &lt;code&gt;persistentvolumeclaims (via ephemeral volumes)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two things follow for operators. The wording changed between releases, so older nodes print the 2021-era phrasing (&lt;code&gt;a mirror pod may not reference service accounts&lt;/code&gt;) and newer ones print the generic form. Grep for &lt;code&gt;mirror pod&lt;/code&gt; and read the suffix to learn which resource tripped it. And that helper returns an error on any spec field it does not recognise, so the check fails closed as new volume types land. That is the design choice that made removing the gate safe three releases after it shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an invisible pod is worse than a failed one
&lt;/h2&gt;

&lt;p&gt;A pod that runs without a Pod object is missing from the entire governance stack. Kyverno and Gatekeeper are admission webhooks on the API server; static pods bypass admission by construction, and the mirror pod is the one API artifact that would have exposed them. CIS benchmark scanners, image inventories built from &lt;code&gt;kubectl get pods -A -o json&lt;/code&gt;, runtime-to-API reconciliation in a SIEM, cost allocation, and every "what is running in my cluster" dashboard read the same source of truth, and that source of truth has a hole in it.&lt;/p&gt;

&lt;p&gt;I care about this more than the raw severity suggests because of what it does to inventory. If you &lt;a href="///blog/generate-an-sbom-with-syft-filter-grype-with-vex.html"&gt;build an SBOM per running image&lt;/a&gt; from an API-server listing, the container you most want to inspect (a hand-edited control-plane component pulling from a private registry) is precisely the one missing from the input list. Add an &lt;code&gt;imagePullSecrets&lt;/code&gt; block to a static pod manifest and the container gets harder to see while staying just as easy to run. That is a strange property for a security boundary, and it is why this landed as a removal rather than a docs clarification.&lt;/p&gt;

&lt;p&gt;The population at risk is narrow. Vanilla kubeadm control-plane manifests use &lt;code&gt;hostPath&lt;/code&gt; volumes only, so a stock cluster upgrades clean. The clusters that break are the ones where someone edited &lt;code&gt;/etc/kubernetes/manifests&lt;/code&gt; by hand: private-registry pulls that added &lt;code&gt;imagePullSecrets&lt;/code&gt; to &lt;code&gt;kube-apiserver.yaml&lt;/code&gt;, air-gapped installs, and vendor node agents shipped as static pods with a &lt;code&gt;projected&lt;/code&gt; serviceAccountToken volume for telemetry. If you have already moved workloads to &lt;a href="///blog/bound-serviceaccount-tokens-9-tips-to-kill-static-ones.html"&gt;bound ServiceAccount tokens&lt;/a&gt;, that projected volume is exactly the pattern you were encouraged to adopt, which is how well-meaning teams ended up here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three causes of "it runs but kubectl cannot see it"
&lt;/h2&gt;

&lt;p&gt;The symptom is one line in a ticket. It has at least three unrelated causes, and the GitHub threads conflate all of them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;An API reference in the manifest.&lt;/strong&gt; The kubelet log shows a create attempt followed by a 403 naming a resource. &lt;code&gt;journalctl -u kubelet | grep -i "mirror pod"&lt;/code&gt; is the whole diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stale mirror pod after a hash mismatch.&lt;/strong&gt; The Pod object exists but describes an older manifest. Compare &lt;code&gt;kubectl get pod &amp;lt;name&amp;gt;-&amp;lt;node&amp;gt; -o jsonpath='{.metadata.annotations.kubernetes\.io/config\.hash}'&lt;/code&gt; against the value after a fresh kubelet restart. The kubelet deletes and recreates on divergence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The kubelet cannot reach the API server, or the Node object is missing.&lt;/strong&gt; Then no mirror pods exist for that node at all, while every static pod still runs. Count them:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="s1"&gt;'[.items[]
  | select(.metadata.annotations["kubernetes.io/config.mirror"])
  | select(.spec.nodeName=="'&lt;/span&gt;&lt;span class="nv"&gt;$NODE&lt;/span&gt;&lt;span class="s1"&gt;'")] | length'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero means cause 3. A partial count means cause 1.&lt;/p&gt;

&lt;p&gt;The check that separates all three without guessing is a diff between runtime truth and API truth, run on the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crictl pods &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.items[].metadata.name'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/runtime.txt
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; spec.nodeName&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$NODE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .items[*]}{.metadata.name}{"\n"}{end}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/api.txt
&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-23&lt;/span&gt; /tmp/runtime.txt /tmp/api.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything in the first list and absent from the second is your invisible set. It takes five seconds per node class, and it is the only inventory that does not inherit the bug. Run it on 1.33 and older too, where nothing else will tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kubelet config line that stops a node from registering
&lt;/h2&gt;

&lt;p&gt;The failure that actually pages you at 03:00 is smaller than the security story. A node whose &lt;code&gt;KubeletConfiguration&lt;/code&gt; still carries &lt;code&gt;PreventStaticPodAPIReferences: false&lt;/code&gt;, pinned during a v1.34 or v1.35 upgrade to defer this work, meets a removed gate on v1.37. The kubelet exits at startup with &lt;code&gt;failed to set feature gates from initial flags-based config: unrecognized feature gate&lt;/code&gt;, and the node never registers. k3s users saw the identical shape when &lt;code&gt;KubeletCredentialProviders&lt;/code&gt; was removed in 1.28.&lt;/p&gt;

&lt;p&gt;This is the same class of upgrade break as the &lt;a href="///blog/fix-unrecognized-format-int32-in-kubernetes-1-34.html"&gt;&lt;code&gt;unrecognized format int32&lt;/code&gt; surprise in 1.34&lt;/a&gt;: a change that is trivially fixable if you find it in a canary node, and miserable if you find it after draining half the control plane. Do the node-side prep first, the way you would for &lt;a href="///blog/kubernetes-user-namespaces-in-1-36-with-hostusers-false.html"&gt;enabling user namespaces with hostUsers false&lt;/a&gt;, and keep the config edit in a separate change from the version bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix in circulation is the wrong artifact
&lt;/h2&gt;

&lt;p&gt;The workaround people reach for is to keep &lt;code&gt;imagePullSecrets&lt;/code&gt; in the manifest and disable the gate. That credential never worked for a static pod in the first place. Registry auth for control-plane images comes from the runtime: &lt;code&gt;/etc/containerd/certs.d/&amp;lt;registry&amp;gt;/hosts.toml&lt;/code&gt;, the &lt;code&gt;registry.configs.&amp;lt;host&amp;gt;.auth&lt;/code&gt; section of &lt;code&gt;config.toml&lt;/code&gt;, or a kubelet credential provider configured with &lt;code&gt;--image-credential-provider-config&lt;/code&gt;. Clusters that "proved" &lt;code&gt;imagePullSecrets&lt;/code&gt; worked were pulling from a warm image cache or an already-authenticated containerd, and the proof evaporates on a rebuilt node.&lt;/p&gt;

&lt;p&gt;Here is my opinion, and it is arguable: I would rather see node-level config and credentials in &lt;code&gt;hostPath&lt;/code&gt; files with 0600 ownership, owned by whatever config management already owns the node, than reachable through the API server. Yes, that means the value is not rotated by a Secret controller and not visible in &lt;code&gt;kubectl describe&lt;/code&gt;. A control-plane component that depends on the API server to fetch the credential it needs to start is a bootstrap loop waiting for a bad day, and the same reasoning applies to any node agent you expect to keep working while the API server is down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to run before you upgrade to 1.37
&lt;/h2&gt;

&lt;p&gt;In this order, on one node per pool, this week:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep the kubelet config for the dead gate.&lt;/strong&gt; &lt;code&gt;grep -n PreventStaticPodAPIReferences /var/lib/kubelet/config.yaml&lt;/code&gt;. A hit means that node fails to start on 1.37. Delete the line, restart the kubelet on the current version, confirm the node stays &lt;code&gt;Ready&lt;/code&gt;, then upgrade. This is the only step that causes an outage if skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep the manifests for references.&lt;/strong&gt; Read &lt;code&gt;staticPodPath&lt;/code&gt; from the config first, because it is not always &lt;code&gt;/etc/kubernetes/manifests&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SPP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/staticPodPath/{print $2}'&lt;/span&gt; /var/lib/kubelet/config.yaml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-REl&lt;/span&gt; &lt;span class="s1"&gt;'secretRef|configMapRef|imagePullSecrets|serviceAccountName|persistentVolumeClaim|projected'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SPP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every file listed is a node that will lose its static pod on 1.37.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the invisible-pod diff&lt;/strong&gt; from the section above on each node class and keep the output. If it is non-empty on a pre-1.34 node, you have been running unreported containers, and your compliance evidence has been under-reporting since 8 July 2021 when the behavior was first written down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move registry credentials into containerd&lt;/strong&gt; (&lt;code&gt;hosts.toml&lt;/code&gt; or a credential provider) and node config into &lt;code&gt;hostPath&lt;/code&gt; files, then delete the offending manifest lines. Verify with a cold pull: &lt;code&gt;crictl rmi &amp;lt;image&amp;gt; &amp;amp;&amp;amp; crictl pull &amp;lt;image&amp;gt;&lt;/code&gt; on a node with no cached layer. A pull that succeeds only because the layer was already local is the failure mode that hides here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add step 3 to your node readiness check permanently.&lt;/strong&gt; Treat a non-empty diff as a failed check, the same way you would treat &lt;a href="///blog/kubernetes-default-deny-egress-stops-pod-exfiltration.html"&gt;a default-deny egress policy that never got applied&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What I read while writing this: the &lt;a href="https://kubernetes.io/blog/2026/07/31/kubernetes-v1-37-sneak-peek/" rel="noopener noreferrer"&gt;v1.37 sneak peek&lt;/a&gt; for the release framing, &lt;a href="https://github.com/kubernetes/kubernetes/pull/140226" rel="noopener noreferrer"&gt;PR #140226&lt;/a&gt; for the removal itself, &lt;a href="https://github.com/kubernetes/kubernetes/issues/103587" rel="noopener noreferrer"&gt;issue #103587&lt;/a&gt; for the 2021 logs, and the &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/static-pods/" rel="noopener noreferrer"&gt;Static Pods documentation&lt;/a&gt; for the rule that was always there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-a-mirror-pod-may-not-reference-secrets-in-k8s-1-37.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
    </item>
    <item>
      <title>Fix Terraform Deposed Object Panic in readDiff on 1.15</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Mon, 24 Aug 2026 02:13:28 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-terraform-deposed-object-panic-in-readdiff-on-115-581e</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-terraform-deposed-object-panic-in-readdiff-on-115-581e</guid>
      <description>&lt;p&gt;On Terraform 1.15.x, &lt;code&gt;terraform apply&lt;/code&gt; can end in a Go stack trace rather than a diagnostic: &lt;code&gt;panic: runtime error: invalid memory address or nil pointer dereference&lt;/code&gt;, with &lt;code&gt;readDiff&lt;/code&gt; in the frame. It is not your config. It is a dropped map key, and the object that key points at is a live cloud resource that almost none of your tooling can see.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; a failed &lt;code&gt;create_before_destroy&lt;/code&gt; leaves an old object in state under a deposed key. If that resource also declares a &lt;code&gt;precondition&lt;/code&gt; or &lt;code&gt;postcondition&lt;/code&gt;, Terraform 1.15.x panics in &lt;code&gt;readDiff&lt;/code&gt; while planning it (&lt;a href="https://github.com/hashicorp/terraform/issues/38586" rel="noopener noreferrer"&gt;hashicorp/terraform#38586&lt;/a&gt;). The fix ships in &lt;a href="https://github.com/hashicorp/terraform/releases/tag/v1.16.0-rc2" rel="noopener noreferrer"&gt;1.16.0-rc2&lt;/a&gt;, released 19 August 2026 and still not GA as of 24 August 2026. Sweep raw state for deposed keys before you upgrade, because 1.16 removes the crash and leaves the orphan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually triggers the readDiff panic
&lt;/h2&gt;

&lt;p&gt;Issue #38586 names the mechanism plainly. The &lt;code&gt;DiffTransformer&lt;/code&gt; builds an update node for the deposed object because conditions exist on the resource. It does not propagate the deposed key onto that node. &lt;code&gt;readDiff&lt;/code&gt; then asks the plan for a change to the &lt;em&gt;current&lt;/em&gt; object, gets nil back, and dereferences it. The reporter filed against Terraform v1.15.2 on darwin_arm64 with AWS provider v6.42.0 and traced the crash to &lt;code&gt;internal/terraform/node_resource_abstract_instance.go:178&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That line matters more than the stack trace does, because it tells you rewriting the &lt;code&gt;precondition&lt;/code&gt; expression will not help. The condition's content is irrelevant. Its existence is the trigger.&lt;/p&gt;

&lt;p&gt;The fix landed in &lt;code&gt;v1.16.0-alpha20260715&lt;/code&gt; on 15 July 2026 and is carried into &lt;code&gt;v1.16.0-rc2&lt;/code&gt;. Every stable Terraform running in production today is on the panicking code path. If a model-generated answer told you this was fixed, check which version it meant.&lt;/p&gt;

&lt;p&gt;Terraform's diagnostics are normally better than a raw panic. Even the genuinely annoying cases, like the &lt;a href="///blog/fix-terraform-the-deprecation-originates-from-warnings.html"&gt;Terraform "The deprecation originates from" warnings&lt;/a&gt; that point at a module which does not own the deprecation, still hand you a message. Here you get a nil dereference in a graph walker, which is the same unhelpful shape as &lt;a href="///blog/fix-npm-cannot-read-properties-of-null-edgesout.html"&gt;npm's "Cannot read properties of null (edgesOut)" crash&lt;/a&gt;: the tool loses a reference mid-walk and takes the process with it. Worse, in CI it usually happens while holding a state lock, under whatever &lt;a href="///blog/github-actions-oidc-to-aws-10-tips-to-kill-static-keys.html"&gt;GitHub Actions OIDC role runs your applies&lt;/a&gt;, so the next job queues behind a lease nobody released.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you confirm a deposed object exists?
&lt;/h2&gt;

&lt;p&gt;One command, and it deliberately bypasses the CLI's own view of state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform state pull &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/state.json
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.resources[] as $r | $r.instances[]
       | select(.deposed != null)
       | "\($r.type).\($r.name) deposed=\(.deposed) id=\(.attributes.id // "unknown")"'&lt;/span&gt; /tmp/state.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your state version serializes it differently, &lt;code&gt;grep -n '"deposed"' /tmp/state.json&lt;/code&gt; settles the question. Zero hits means the deposed theory is dead and your panic is something else. One or more hits, on a resource that also carries a &lt;code&gt;precondition&lt;/code&gt; or &lt;code&gt;postcondition&lt;/code&gt;, is #38586.&lt;/p&gt;

&lt;p&gt;For the plan side there is a documented path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform show &lt;span class="nt"&gt;-json&lt;/span&gt; tfplan | jq &lt;span class="s1"&gt;'.resource_changes[] | select(.deposed != null)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What I actually read.&lt;/strong&gt; &lt;a href="https://github.com/hashicorp/terraform/issues/38586" rel="noopener noreferrer"&gt;Issue #38586&lt;/a&gt; for the &lt;code&gt;DiffTransformer&lt;/code&gt; and &lt;code&gt;readDiff&lt;/code&gt; mechanism and the exact source line, and &lt;a href="https://github.com/hashicorp/terraform/issues/29713" rel="noopener noreferrer"&gt;issue #29713&lt;/a&gt; for the &lt;code&gt;missing deposed state&lt;/code&gt; variant, where core maintainer jbardin confirms the deposed entry was readable during &lt;code&gt;walkPlanDestroy&lt;/code&gt; and gone by &lt;code&gt;walkDestroy&lt;/code&gt;. The visibility rules come from HashiCorp's own &lt;a href="https://developer.hashicorp.com/terraform/internals/json-format" rel="noopener noreferrer"&gt;JSON output format reference&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three error strings, one dropped deposed key
&lt;/h2&gt;

&lt;p&gt;Operators treat these as one bug. They need different responses.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error string&lt;/th&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Where it fails&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;panic: ... nil pointer dereference&lt;/code&gt; with &lt;code&gt;readDiff&lt;/code&gt; in frame&lt;/td&gt;
&lt;td&gt;#38586&lt;/td&gt;
&lt;td&gt;Plan graph builds an update node for the deposed object, key not propagated&lt;/td&gt;
&lt;td&gt;Drop the condition block for one apply, or move to 1.16.0-rc2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Error: missing deposed state for aws_api_gateway_rest_api.this (4ece7f68)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#29713&lt;/td&gt;
&lt;td&gt;Entry readable in &lt;code&gt;walkPlanDestroy&lt;/code&gt;, absent in &lt;code&gt;walkDestroy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Re-plan; the deposed entry is real, the destroy walk lost it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;panic: MaybeRestoreResourceInstanceDeposed called without DeposedKey&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;#22974&lt;/td&gt;
&lt;td&gt;Older restore path, reported 2019&lt;/td&gt;
&lt;td&gt;Historical, same defect class&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Seven years of the same failure: the deposed key does not travel with the object it identifies. That is worth knowing before you attribute the crash to your module, your provider, or your backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why terraform state list will never show it
&lt;/h2&gt;

&lt;p&gt;The plan representation and the state representation disagree, and the disagreement is documented. HashiCorp's JSON format reference calls the deposed key "an opaque key representing the specific deposed object" and notes that address plus deposed key together uniquely identify a change. The values representation, which is what &lt;code&gt;terraform show -json&lt;/code&gt; gives you for state, says the opposite in plain words: "Only the 'current' object for each resource instance is described. 'Deposed' objects are not reflected in this structure at all."&lt;/p&gt;

&lt;p&gt;So your inventory tool, drift detector, cost report and tag-compliance scan all read a document that structurally cannot mention the resource you are paying for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["Config change forces replacement\nlifecycle create_before_destroy"] --&amp;gt; B["New object created\nappears in state list"]
  B --&amp;gt; C["Old object moved aside\nunder deposed key 4ece7f68"]
  C --&amp;gt; D{"Destroy of old object succeeds?"}
  D --&amp;gt;|"yes"| E["Deposed entry removed\nstate list and show -json agree"]
  D --&amp;gt;|"no"| F["Deposed entry persists\ncloud resource still running and billed"]
  F --&amp;gt; G["Invisible: terraform state list\nand show -json values representation"]
  F --&amp;gt; H["Visible: plan JSON deposed field\nand terraform state pull raw JSON"]
  F --&amp;gt; I["On 1.15.x with a condition block\napply panics in readDiff"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also no address you can type. The &lt;a href="https://developer.hashicorp.com/terraform/cli/state/resource-addressing" rel="noopener noreferrer"&gt;resource addressing reference&lt;/a&gt; documents module paths, type and name, numeric &lt;code&gt;count&lt;/code&gt; indices and string &lt;code&gt;for_each&lt;/code&gt; keys. The word "deposed" does not appear on that page, and the &lt;code&gt;terraform state rm&lt;/code&gt; documentation never mentions it either. Every "just &lt;code&gt;state rm&lt;/code&gt; the deposed one" answer in circulation describes a grammar that does not exist.&lt;/p&gt;

&lt;p&gt;Policy as code inherits the blind spot in a weaker form. The Sentinel &lt;a href="https://developer.hashicorp.com/sentinel/docs/features/terraform/tfstate-v2" rel="noopener noreferrer"&gt;&lt;code&gt;tfstate/v2&lt;/code&gt; import&lt;/a&gt; exposes a &lt;code&gt;deposed_key&lt;/code&gt; attribute on a resource, described as matching the &lt;code&gt;deposed&lt;/code&gt; field in &lt;code&gt;resource_changes&lt;/code&gt;. It is a flag, not a collection. There is no deposed object whose attributes a policy can evaluate, so a rule like "every instance must carry an owner tag" cannot see the untagged orphan at all. If you are already tracking &lt;a href="///blog/non-human-identity-governance-field-tips-for-2026.html"&gt;cloud resources that no identity or team owns&lt;/a&gt;, this is a category your governance stack is silently missing.&lt;/p&gt;

&lt;p&gt;I have watched a team spend a quarter arguing about an unexplained line item in an AWS bill that turned out to be a deposed NAT gateway from a botched replacement. Their scanner was correct. Their state was correct. The interchange format between them omitted the object.&lt;/p&gt;

&lt;h2&gt;
  
  
  The documented recovery deletes your only evidence
&lt;/h2&gt;

&lt;p&gt;HashiCorp's support article on &lt;code&gt;create_before_destroy&lt;/code&gt; and deposed objects walks operators through removing &lt;em&gt;all&lt;/em&gt; instances of the resource from state, importing the wanted object back, then deleting the unwanted one by hand in the cloud console. Its old URL now 302s into IBM's support portal, which tells you how much attention this corner gets.&lt;/p&gt;

&lt;p&gt;Step one erases the deposed object's attributes, and that is where the real resource ID was written down. Step three then depends on a human remembering an ID that no longer exists anywhere in the repo, the state, or the plan. If you follow that procedure without recording the ID first, you have converted a tracked orphan into an untracked one.&lt;/p&gt;

&lt;p&gt;Related behavior worth naming while you are in here: &lt;a href="https://github.com/hashicorp/terraform/issues/31635" rel="noopener noreferrer"&gt;issue #31635&lt;/a&gt; reports that with &lt;code&gt;create_before_destroy&lt;/code&gt;, a failure in a &lt;em&gt;later&lt;/em&gt; resource still leaves Terraform destroying the deposed object. Replacement ordering and failure handling around this feature are less deterministic than the lifecycle documentation implies. Deposed leftovers are common, not exotic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading to 1.16 makes this quieter, not cleaner
&lt;/h2&gt;

&lt;p&gt;The panic was a loud, unignorable signal that a deposed object existed. On 1.16 the same state applies cleanly and the object is still there, still running, still outside configuration. My read is that most teams will upgrade, watch the error disappear, and never learn the orphan was there. Given the choice I would keep the crash, which is why the jq check belongs in CI before the upgrade rather than after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to run before you move CI to 1.16
&lt;/h2&gt;

&lt;p&gt;Ordered by what bites first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sweep every state you own now, while the panic is still there to get your attention.&lt;/strong&gt; Run the &lt;code&gt;terraform state pull&lt;/code&gt; plus jq check above against each workspace. Any hit is a running resource with no configuration governing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record &lt;code&gt;attributes.id&lt;/code&gt;, region and ARN in a ticket before any state surgery.&lt;/strong&gt; Not in terminal scrollback. &lt;code&gt;terraform state rm&lt;/code&gt; removes current and deposed instances together, and that command is the moment the ID stops existing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer letting Terraform destroy it over removing it from state.&lt;/strong&gt; If the remote object still exists, keep the resource in configuration and let a normal apply plan the destroy of the deposed object. State surgery is the fallback, not the first move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If that apply panics in &lt;code&gt;readDiff&lt;/code&gt; on 1.15.x, comment out the &lt;code&gt;precondition&lt;/code&gt; or &lt;code&gt;postcondition&lt;/code&gt; block on that one resource, apply, then restore it.&lt;/strong&gt; This follows from the mechanism in #38586: no conditions means no update node for the deposed object. You are disabling a safety check to clear a leftover, so test against a copy of state first and put the block back in the same pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hold CI on 1.15.x until 1.16.0 goes GA&lt;/strong&gt;, then upgrade with the deposed sweep already wired in as a post-apply step. Alerting threshold: a deposed key that survives two consecutive applies is an orphan, not a transient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Point drift and cost tooling at plan JSON &lt;code&gt;resource_changes[].deposed&lt;/code&gt; or at raw state&lt;/strong&gt;, never at the values representation of &lt;code&gt;terraform show -json&lt;/code&gt;, which is documented to omit these objects.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is a deposed object in Terraform?&lt;/strong&gt; When a resource with &lt;code&gt;create_before_destroy&lt;/code&gt; is replaced, Terraform creates the new object first and moves the old one aside under a deposed key, an 8-character hex string such as &lt;code&gt;4ece7f68&lt;/code&gt;. If the destroy of the old object then fails, that entry stays in state and the real cloud resource keeps running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;terraform state list&lt;/code&gt; not show deposed objects?&lt;/strong&gt; No address grammar exists for them, and the values representation of &lt;code&gt;terraform show -json&lt;/code&gt; is documented to describe only the current object per instance. Their absence is by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it safe to run &lt;code&gt;terraform state rm&lt;/code&gt; on a deposed resource?&lt;/strong&gt; There is no syntax to remove only the deposed object, so &lt;code&gt;state rm&lt;/code&gt; drops current and deposed together and takes the real resource ID with it. Record the ID first and prefer a planned destroy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Terraform 1.16 remove deposed objects automatically?&lt;/strong&gt; No. The release candidates fix the nil pointer dereference so the apply completes. The orphan survives the upgrade.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-terraform-deposed-object-panic-in-readdiff-on-1-15.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>Fix Terraform "The deprecation originates from" Warnings</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Mon, 24 Aug 2026 02:09:04 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-terraform-the-deprecation-originates-from-warnings-2545</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-terraform-the-deprecation-originates-from-warnings-2545</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Terraform 1.15 reports a deprecated attribute you never referenced because deprecation rides inside the value as a cty mark, and exporting a whole resource object carries the mark with it. Three different situations print the identical warning. Run &lt;code&gt;terraform validate -json&lt;/code&gt;, compare &lt;code&gt;warning_count&lt;/code&gt; to the console, then grep each &lt;code&gt;originates from&lt;/code&gt; attribute against your own HCL: hits are real migration work, misses are propagation. Terraform 1.16 fixes the child module case only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Nobody wrote &lt;code&gt;website_domain&lt;/code&gt;. Terraform 1.15.8 printed it anyway, attached to an output block that exports an S3 bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: Deprecated value used

  on bucket.tf line 11, in output "bucket":
  11:   value       = aws_s3_bucket.example

  The deprecation originates from aws_s3_bucket.example.website_domain

website_domain is deprecated. [more deprecation instructions here]

(and 13 more similar warnings elsewhere)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is &lt;a href="https://github.com/hashicorp/terraform/issues/38982" rel="noopener noreferrer"&gt;hashicorp/terraform issue #38982&lt;/a&gt;, and the last line matters as much as the text above it. &lt;a href="https://github.com/hashicorp/terraform/issues/38554" rel="noopener noreferrer"&gt;Issue #38554&lt;/a&gt;, filed 2026-05-11 against 1.15.2, counted roughly 30 of these coming out of one S3 module, and the reporter's summary does the diagnosis for you: "no value is passed to the variable, it is never referenced, so no warning should be produced." One warning summary with several unrelated triggers is the same trap as &lt;a href="///blog/fix-npm-cannot-read-properties-of-null-edgesout.html"&gt;npm's null edgesOut crash&lt;/a&gt;. You cannot fix it until you know which trigger you have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Terraform blames an attribute you never wrote
&lt;/h2&gt;

&lt;p&gt;Terraform 1.15 shipped variable and output deprecation in PR #38001, and it is implemented with marks rather than usage analysis. &lt;code&gt;internal/lang/marks&lt;/code&gt; defines a &lt;code&gt;DeprecationMark{Message, OriginDescription}&lt;/code&gt;, and deprecated provider schema attributes get the same treatment. A mark lives inside the value. Export the resource object and the mark on &lt;code&gt;website_domain&lt;/code&gt; goes with it, so when the output node walks the value looking for marks, it finds one and reports the export as a use.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;The deprecation originates from&lt;/code&gt; line is literally the mark's &lt;code&gt;OriginDescription&lt;/code&gt;. That is the reason it names an attribute that appears nowhere in your configuration, and the reason no amount of editing your own HCL clears it.&lt;/p&gt;

&lt;p&gt;The blast radius is a module you did not write. The terraform-aws-eventbridge module tripped this at version 4.3.0 on Terraform 1.15.4 with AWS provider 6.46.0 from a single line, &lt;code&gt;output "eventbridge_rules" { value = aws_cloudwatch_event_rule.this }&lt;/code&gt;, as filed in &lt;a href="https://github.com/terraform-aws-modules/terraform-aws-eventbridge/issues/199" rel="noopener noreferrer"&gt;module issue #199&lt;/a&gt;. Any module that exports whole resource objects, which is most of them, becomes a warning generator the moment a provider deprecates one nested attribute. And a deprecation is a removal clock, the same kind of clock that &lt;a href="///blog/migrate-ingress-nginx-to-gateway-api-before-retirement.html"&gt;Ingress-NGINX retirement&lt;/a&gt; put on everyone's ingress layer, so the noise is worth reading rather than muting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 1.16 changelog line actually covers
&lt;/h2&gt;

&lt;p&gt;Terraform 1.16.0-rc2 landed on 2026-08-19 with a changelog entry that reads like the end of this: "Child module outputs with unreferenced deprecated nested attributes no longer return deprecation warnings" (#38778).&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/hashicorp/terraform/pull/38778" rel="noopener noreferrer"&gt;merged diff&lt;/a&gt; is eleven lines in &lt;code&gt;internal/terraform/node_output.go&lt;/code&gt; (merged 2026-06-25, approved by jbardin), and it branches on &lt;code&gt;IsRoot()&lt;/code&gt;:&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;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModulePath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsRoot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deprecationDiags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deprecations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidateExpressionDeepAndUnmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModulePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// If the output is in a child module, only check for deprecations&lt;/span&gt;
    &lt;span class="c"&gt;// at the "top level". This avoids deprecation warnings when&lt;/span&gt;
    &lt;span class="c"&gt;// outputting an entire resource with a nested deprecated attribute.&lt;/span&gt;
    &lt;span class="c"&gt;// (References to said attribute should still incur a warning)&lt;/span&gt;
    &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deprecationDiags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deprecations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidateAndUnmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModulePath&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ptr&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;The doc comments in &lt;code&gt;internal/deprecation/deprecation.go&lt;/code&gt; name the difference plainly: &lt;code&gt;ValidateExpressionDeepAndUnmark&lt;/code&gt; uses &lt;code&gt;GetDeprecationMarksDeep&lt;/code&gt; and traverses the value, &lt;code&gt;ValidateAndUnmark&lt;/code&gt; uses &lt;code&gt;GetDeprecationMarks&lt;/code&gt; and handles top-level marks only. Root outputs keep the deep walk.&lt;/p&gt;

&lt;p&gt;The repro in #38982 is a root module output (&lt;code&gt;on bucket.tf line 11, in output "bucket"&lt;/code&gt;), and my read of the diff is that this case still warns on 1.16 even though the issue is closed. I would spend ten minutes reproducing your own case on rc2 before you schedule an upgrade around that changelog line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["aws_s3_bucket.example\nprovider marks website_domain deprecated"] --&amp;gt; B{"where is the\noutput block?"}
  B --&amp;gt;|"child module"| C["ValidateAndUnmark\ntop-level marks only\nno warning in 1.16"]
  B --&amp;gt;|"root module"| D["ValidateExpressionDeepAndUnmark\nwalks nested marks\nwarning still fires"]
  C --&amp;gt; E["nested marks survive into the caller"]
  E --&amp;gt; F["caller re-exports the object\nfrom a root output"]
  F --&amp;gt; D
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth noting that the 1.15 line moved the opposite way in the same week. Terraform 1.15.9, released 2026-08-19 alongside rc2, carries "Child module validation has been fixed and will now raise errors or warning diagnostics for invalid blocks." Patch forward on 1.15 and you get more child module diagnostics; move to 1.16 and you get fewer of this specific kind. 1.15.6 had already fixed a &lt;code&gt;terraform console&lt;/code&gt; panic when evaluating expressions with deprecated values. Four minor releases in, the marks implementation is still settling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three causes hide behind one warning summary
&lt;/h2&gt;

&lt;p&gt;The issue threads run these together, which is why the advice inside them contradicts itself.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;What the "originates from" path points at&lt;/th&gt;
&lt;th&gt;Fixed in 1.16.0&lt;/th&gt;
&lt;th&gt;The right move&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You actually reference the deprecated attribute&lt;/td&gt;
&lt;td&gt;An attribute you can grep in your own HCL&lt;/td&gt;
&lt;td&gt;No, and correctly so&lt;/td&gt;
&lt;td&gt;Migrate the attribute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A child module output exports a whole resource or data source object&lt;/td&gt;
&lt;td&gt;An attribute of a resource inside the module&lt;/td&gt;
&lt;td&gt;Yes (#38778)&lt;/td&gt;
&lt;td&gt;Upgrade Terraform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A root module output or local exports a whole object&lt;/td&gt;
&lt;td&gt;An attribute of a resource in your root config&lt;/td&gt;
&lt;td&gt;Not per the diff&lt;/td&gt;
&lt;td&gt;Export named attributes, or accept the noise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The two commands that separate them
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;terraform validate -json&lt;/code&gt; and compare its &lt;code&gt;warning_count&lt;/code&gt; against what the console showed you. The human renderer consolidates same-summary diagnostics into &lt;code&gt;(and N more similar warnings elsewhere)&lt;/code&gt;, a behavior still tracked as open &lt;a href="https://github.com/hashicorp/terraform/issues/32104" rel="noopener noreferrer"&gt;issue #32104&lt;/a&gt;, so the collapsed remainder is exactly where a genuine deprecation hides behind a dozen propagation artifacts. If the console printed 4 and the JSON says 30, you have been reading 13 percent of your deprecation surface.&lt;/p&gt;

&lt;p&gt;Then pull the attribute name out of each &lt;code&gt;originates from&lt;/code&gt; path and grep your own HCL for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform validate &lt;span class="nt"&gt;-json&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.diagnostics[] | select(.summary=="Deprecated value used") | .detail'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s1"&gt;'originates from [^ ]*'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero grep hits in your configuration means propagation. Hits mean migration work. That split is the only thing that tells you whether a Terraform upgrade helps you at all, and it takes about a minute per workspace inside the same &lt;a href="///blog/github-actions-oidc-to-aws-10-tips-to-kill-static-keys.html"&gt;CI job that already runs plan against AWS&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ignore_nested_deprecations deletes the signal, it does not hide it
&lt;/h2&gt;

&lt;p&gt;The workaround circulating in the module threads is &lt;code&gt;ignore_nested_deprecations = true&lt;/code&gt; on the module call. The syntax page gives it one sentence: "If the &lt;code&gt;ignore_nested_deprecations&lt;/code&gt; argument is set to true, Terraform does not show deprecation warnings in the module call or nested modules." No mention of scope, and nothing about what happens to the underlying data.&lt;/p&gt;

&lt;p&gt;The source is more specific than the docs. Suppression runs through &lt;code&gt;IsModuleCallDeprecationSuppressed&lt;/code&gt;, checked against a &lt;code&gt;suppressedModules&lt;/code&gt; list, and both validation paths remove the marks from the value whether or not suppression is on. This is not a display filter. It strips the deprecation information for that module and everything under it, real deprecations included. Set it to quiet fourteen false positives and you have also disabled the fifteenth, which was the one you needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  These warnings are your AWS provider v7 backlog
&lt;/h2&gt;

&lt;p&gt;Ignoring them is tempting because they do not change an exit code. Plan stays green, CI stays green.&lt;/p&gt;

&lt;p&gt;The AWS provider is currently using deprecations as its v7 removal queue. &lt;a href="https://github.com/hashicorp/terraform-provider-aws/issues/42468" rel="noopener noreferrer"&gt;Provider issue #42468&lt;/a&gt;, milestone v7.0.0, is open to delete &lt;code&gt;name&lt;/code&gt; from the &lt;code&gt;aws_region&lt;/code&gt; data source. Issue #44803 tracks removing deprecated &lt;code&gt;status&lt;/code&gt; attributes from the organizations resources. Each of those turns into a hard plan error on the day you bump the major version, and the warnings you are scrolling past are the inventory of that work. It is the same warning-window economics as the &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;npm allowScripts notices before npm 12&lt;/a&gt;: free to fix while it is a warning, a broken pipeline once the major lands. Suppress the list at the module boundary in August and you rediscover it as an upgrade-day outage, which is how &lt;a href="///blog/fix-unrecognized-format-int32-in-kubernetes-1-34.html"&gt;a Kubernetes minor bump turns into an "unrecognized format int32" scramble&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to run before your next provider bump
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Capture the real count. &lt;code&gt;terraform validate -json | jq '.warning_count'&lt;/code&gt; in every workspace, and record the delta against the console number. Anything above roughly 10 means the console has been collapsing most of it (#32104).&lt;/li&gt;
&lt;li&gt;Classify with grep, not by reading. Extract each &lt;code&gt;originates from&lt;/code&gt; attribute and search your own HCL. Hits go on the migration list; misses are mark propagation and no config edit will clear them.&lt;/li&gt;
&lt;li&gt;Fix the ones you own at the source. In your own modules, export named attributes instead of whole resource objects wherever the consumer contract allows. That kills the noise on every Terraform version, including the root module case 1.16 does not cover.&lt;/li&gt;
&lt;li&gt;Leave &lt;code&gt;ignore_nested_deprecations&lt;/code&gt; alone unless the module is vendored and unchangeable. If you must set it, put the date you will revisit it in a comment next to the argument, because it blinds the entire subtree.&lt;/li&gt;
&lt;li&gt;Turn the surviving genuine warnings into a v7 ticket now. Provider issues #42468 and #44803 are open against the v7.0.0 milestone; every attribute on that list is a plan error waiting for your major bump.&lt;/li&gt;
&lt;li&gt;If a 1.16 upgrade is on the plan specifically to clear this, reproduce your case on 1.16.0-rc2 first. The changelog says child module outputs, and the eleven-line diff means it: &lt;a href="https://github.com/hashicorp/terraform/pull/38778" rel="noopener noreferrer"&gt;https://github.com/hashicorp/terraform/pull/38778&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-terraform-the-deprecation-originates-from-warnings.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>npm approve-scripts vs --allow-scripts: pick the right flag</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Sun, 23 Aug 2026 08:36:31 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/npm-approve-scripts-vs-allow-scripts-pick-the-right-flag-3175</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/npm-approve-scripts-vs-allow-scripts-pick-the-right-flag-3175</guid>
      <description>&lt;p&gt;npm 12 blocks dependency install scripts by default and gives you four different ways to let one run again. Two of those ways reject each other outright, so what follows is the decision rule: which command inside a project, which flag for a global install, what actually lands in git, and the CI gate that catches what the review command quietly hides from you.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Inside a project, never pass &lt;code&gt;--allow-scripts&lt;/code&gt; on the command line. Run &lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; to review, then &lt;code&gt;npm approve-scripts &amp;lt;pkg&amp;gt;&lt;/code&gt; (or &lt;code&gt;--all&lt;/code&gt;) to write the &lt;code&gt;allowScripts&lt;/code&gt; field into &lt;code&gt;package.json&lt;/code&gt;, and commit &lt;code&gt;package.json&lt;/code&gt;. For a global install (&lt;code&gt;-g&lt;/code&gt;) or &lt;code&gt;npx&lt;/code&gt; there is no &lt;code&gt;package.json&lt;/code&gt; to write to, so you must use the install-time &lt;code&gt;--allow-scripts=&amp;lt;pkg&amp;gt;&lt;/code&gt; flag, the same flag that is forbidden inside a project. Gate CI with &lt;code&gt;npm ci --strict-allow-scripts&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The confusion is worth naming precisely, because the two failures look similar and have opposite fixes. &lt;code&gt;npm approve-scripts&lt;/code&gt; exits &lt;code&gt;EGLOBAL&lt;/code&gt; when you run it for a global install (&lt;a href="https://github.com/npm/cli/issues/9463" rel="noopener noreferrer"&gt;npm/cli #9463&lt;/a&gt;). &lt;code&gt;--allow-scripts&lt;/code&gt; is refused inside a project install, with an error telling you to edit &lt;code&gt;package.json&lt;/code&gt; instead. Same person, two terminals, two contradictory messages. If you have already hit the second one, the walkthrough lives in &lt;a href="///blog/fix-npm---allow-scripts-not-allowed-in-project-installs.html"&gt;Fix npm --allow-scripts not allowed in project installs&lt;/a&gt;; the reason the field belongs in &lt;code&gt;package.json&lt;/code&gt; rather than where most people reach for it is covered in &lt;a href="///blog/npm-allowscripts-the-package-json-schema-not-npmrc.html"&gt;npm allowScripts: the package.json schema, not .npmrc&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which flag, in which scope?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Mutates?&lt;/th&gt;
&lt;th&gt;What it touches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;See what needs approval (read-only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; or &lt;code&gt;npm install-scripts ls&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approve one package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm approve-scripts &amp;lt;pkg&amp;gt;&lt;/code&gt; / &lt;code&gt;npm install-scripts approve &amp;lt;pkg&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allowScripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approve everything pending&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm approve-scripts --all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allowScripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permanently deny a package&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;npm deny-scripts &amp;lt;pkg&amp;gt;&lt;/code&gt; / &lt;code&gt;npm install-scripts deny &amp;lt;pkg&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allowScripts&lt;/code&gt; (name-only &lt;code&gt;false&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allow scripts for a global or npx install&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm i -g --allow-scripts=&amp;lt;pkg&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;nothing persisted (per invocation)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persist a global allowance&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm config set allow-scripts=&amp;lt;pkg&amp;gt; --location=user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;your user &lt;code&gt;.npmrc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enforce in CI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm ci --strict-allow-scripts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;project&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;fails the build on gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model to keep in your head: &lt;code&gt;approve-scripts&lt;/code&gt; writes a committed allowlist into &lt;code&gt;package.json&lt;/code&gt;, while &lt;code&gt;--allow-scripts&lt;/code&gt; is a one-off install-time override for contexts that have no &lt;code&gt;package.json&lt;/code&gt; to commit. Each one is rejected in the other's territory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Install script was skipped"] --&amp;gt; B{"Global install or npx?"}
    B --&amp;gt;|"global"| C["npm i -g --allow-scripts=pkg"]
    B --&amp;gt;|"project"| D["npm approve-scripts --allow-scripts-pending\n(review only)"]
    D --&amp;gt; E["npm approve-scripts pkg\nor --all"]
    E --&amp;gt; F["Commit package.json"]
    F --&amp;gt; G["CI: npm ci --strict-allow-scripts"]
    C --&amp;gt; H["npm config set allow-scripts=pkg --location=user\nto persist per machine"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm 12.x&lt;/strong&gt; (&lt;code&gt;npm -v&lt;/code&gt;). The &lt;code&gt;npm install-scripts&lt;/code&gt; command namespace and the &lt;code&gt;strict-allow-scripts&lt;/code&gt; config are npm 12 features, shipped with the breaking changes the npm team published on June 9, 2026 (&lt;a href="https://github.blog/changelog/2026-06-09-upcoming-breaking-changes-for-npm-v12/" rel="noopener noreferrer"&gt;GitHub Changelog&lt;/a&gt;). On 11.16.x you get warnings only.&lt;/li&gt;
&lt;li&gt;Node.js 22+ (bundled with npm 12).&lt;/li&gt;
&lt;li&gt;A repo with a &lt;code&gt;package.json&lt;/code&gt; and a &lt;code&gt;package-lock.json&lt;/code&gt; for the project workflow, plus global install access for the &lt;code&gt;-g&lt;/code&gt; case.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Inventory what would run, without writing anything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm approve-scripts &lt;span class="nt"&gt;--allow-scripts-pending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lists every dependency whose install scripts are not yet covered by &lt;code&gt;allowScripts&lt;/code&gt; and changes nothing on disk. &lt;code&gt;npm install-scripts ls&lt;/code&gt; is the equivalent under the namespaced command. Run it first, every time. It is the audit output you paste into the PR that adds the allowlist, so a reviewer can see what you decided to trust and what you left out.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Approve only the packages you actually trust
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# one at a time, the safe default&lt;/span&gt;
npm approve-scripts esbuild

&lt;span class="c"&gt;# or approve everything pending in one shot&lt;/span&gt;
npm approve-scripts &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Either form writes entries into the &lt;code&gt;allowScripts&lt;/code&gt; object in &lt;code&gt;package.json&lt;/code&gt;. Approvals are version-pinned by default (&lt;code&gt;--allow-scripts-pin&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;), producing &lt;code&gt;"esbuild@0.25.0": true&lt;/code&gt;. Setting &lt;code&gt;--allow-scripts-pin=false&lt;/code&gt; gives you name-only entries that survive version bumps, which is more convenient and strictly weaker: you lose the property that a new version drops back into "pending" and gets re-reviewed. I keep pinning on and treat the churn as the feature, because a &lt;code&gt;postinstall&lt;/code&gt; added in a patch release is exactly the event an allowlist exists to catch. That is the same fast-publish window that &lt;a href="///blog/dependency-cooldowns-beat-fast-supply-chain-attacks.html"&gt;dependency cooldowns&lt;/a&gt; close from the release-timing side.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--all&lt;/code&gt; is fine on the first migration of a repo you already know. It is a bad habit after that, since it approves whatever happens to be pending rather than whatever you read.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Deny what should never run, and know the asymmetry
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm deny-scripts core-js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A denial is always written name-only as &lt;code&gt;"core-js": false&lt;/code&gt;, whatever the pin setting says. That asymmetry is deliberate and useful: &lt;code&gt;--all&lt;/code&gt; approves everything pending, but a &lt;code&gt;false&lt;/code&gt; entry is sticky and is not overwritten by a later &lt;code&gt;--all&lt;/code&gt;. Deny is the permanent "no" that a blanket approve cannot undo by accident.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Commit the one artifact this produces
&lt;/h3&gt;

&lt;p&gt;The only thing that gets committed is the &lt;code&gt;allowScripts&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt;. Here is a real one, from the eslint project's npm 12 migration (&lt;a href="https://github.com/eslint/eslint/pull/21092" rel="noopener noreferrer"&gt;eslint/eslint#21092&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowScripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"core-js"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cypress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"re2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"yorkie"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit &lt;code&gt;package.json&lt;/code&gt;. Do not adopt hand-editing as a workflow, let the commands maintain the field, but do review the diff the way you would review an IAM policy change: each &lt;code&gt;true&lt;/code&gt; is a package you have granted arbitrary code execution on every developer laptop and every CI runner. The error message from a mis-run install names the two legal homes, &lt;code&gt;allowScripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; or &lt;code&gt;.npmrc&lt;/code&gt;. Keep the project allowlist in &lt;code&gt;package.json&lt;/code&gt; where it is committed, PR-reviewed, and shared, and reserve &lt;code&gt;.npmrc&lt;/code&gt; for machine-local or global allowances that should not travel with the repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Gate CI so an unapproved script fails the build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci &lt;span class="nt"&gt;--strict-allow-scripts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a normal install npm skips unapproved scripts, prints a summary, and exits &lt;code&gt;0&lt;/code&gt;. A green local &lt;code&gt;npm install&lt;/code&gt; therefore proves nothing about whether your allowlist is complete. &lt;code&gt;--strict-allow-scripts&lt;/code&gt; (or &lt;code&gt;npm config set strict-allow-scripts true&lt;/code&gt;) turns a skipped script into a hard failure, so CI catches the dependency that started shipping a &lt;code&gt;postinstall&lt;/code&gt; between two commits instead of letting a native module go missing until it explodes inside a request at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does npm approve-scripts fail on a global install?
&lt;/h2&gt;

&lt;p&gt;Install a global CLI on npm 12 and you get a warning that points you at the wrong command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; some-cli
&lt;span class="c"&gt;# npm warn allow-scripts Run `npm approve-scripts --allow-scripts-pending` to&lt;/span&gt;
&lt;span class="c"&gt;#   review, or `npm approve-scripts &amp;lt;pkg&amp;gt;` to allow.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow that advice and you hit npm/cli &lt;a href="https://github.com/npm/cli/issues/9463" rel="noopener noreferrer"&gt;#9463&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm approve-scripts does not work for global installs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It exits with code &lt;code&gt;EGLOBAL&lt;/code&gt;. The reason is structural rather than a bug in your invocation: &lt;code&gt;approve-scripts&lt;/code&gt; writes to a project &lt;code&gt;package.json&lt;/code&gt;, and a global install does not have one. The suggested command cannot work in that context, so treat the warning text as wrong there. The working fix is the install-time flag, optionally persisted to your user &lt;code&gt;.npmrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;--allow-scripts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;some-cli some-cli

&lt;span class="c"&gt;# persist it for future global installs of that package&lt;/span&gt;
npm config &lt;span class="nb"&gt;set &lt;/span&gt;allow-scripts&lt;span class="o"&gt;=&lt;/span&gt;some-cli &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror image is the project-scoped rejection of that same flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error --allow-scripts is not allowed in project-scoped installs.
Add the entries to the "allowScripts" field in package.json, or to .npmrc, instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One rule resolves both messages: project goes through &lt;code&gt;approve-scripts&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt;, global and npx go through &lt;code&gt;--allow-scripts&lt;/code&gt; at install time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. the allowlist is populated&lt;/span&gt;
node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"Object.keys(require('./package.json').allowScripts || {}).length"&lt;/span&gt;

&lt;span class="c"&gt;# 2. strict CI passes with nothing pending&lt;/span&gt;
npm ci &lt;span class="nt"&gt;--strict-allow-scripts&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK: all scripts covered"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first prints a non-zero count. The second exits &lt;code&gt;0&lt;/code&gt; with no &lt;code&gt;install scripts not covered&lt;/code&gt; line. If the second command fails while &lt;code&gt;--allow-scripts-pending&lt;/code&gt; showed you nothing, that is not a mistake on your part, it is the pitfall below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The review command hides optional deps that the strict check rejects.&lt;/strong&gt; This is the sharpest trap in the feature, filed as npm/cli &lt;a href="https://github.com/npm/cli/issues/9562" rel="noopener noreferrer"&gt;#9562&lt;/a&gt;. On Linux, &lt;code&gt;npm approve-scripts --allow-scripts-pending&lt;/code&gt; does not list &lt;code&gt;fsevents&lt;/code&gt;, because it is an optional dependency marked &lt;code&gt;os: ["darwin"]&lt;/code&gt; and your platform skips scanning it. But &lt;code&gt;npm ci --strict-allow-scripts&lt;/code&gt; validates the lockfile, sees &lt;code&gt;fsevents&lt;/code&gt; in there, and fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm error --strict-allow-scripts: 1 package(s) have install scripts not covered by allowScripts: fsevents@2.3.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your review said "all clear" and CI said "rejected" for the same dependency tree. Approve the optional package explicitly even though the inventory never surfaced it (&lt;code&gt;npm approve-scripts fsevents@2.3.3&lt;/code&gt;), or run the inventory on the OS where that optional dependency actually installs. When the two disagree, the lockfile-based strict check is the source of truth and the platform-filtered review is the false negative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version pins go stale after upgrades.&lt;/strong&gt; With pinning on, bumping &lt;code&gt;esbuild@0.25.0&lt;/code&gt; to &lt;code&gt;0.26.0&lt;/code&gt; moves it back to pending, which is intended. Clear dead entries with &lt;code&gt;npm install-scripts prune&lt;/code&gt;, and preview with &lt;code&gt;--dry-run&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A passing &lt;code&gt;npm install&lt;/code&gt; is not proof of coverage.&lt;/strong&gt; Non-strict installs skip and warn while still exiting &lt;code&gt;0&lt;/code&gt;. Only &lt;code&gt;--strict-allow-scripts&lt;/code&gt; makes coverage a gate, so wire it into the pipeline rather than into your own shell aliases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The decision rule is short: &lt;code&gt;approve-scripts&lt;/code&gt; plus &lt;code&gt;package.json&lt;/code&gt; for projects, &lt;code&gt;--allow-scripts&lt;/code&gt; for global and npx, &lt;code&gt;deny-scripts&lt;/code&gt; for the permanent no, &lt;code&gt;--strict-allow-scripts&lt;/code&gt; for CI. In that order, and with the pin left on.&lt;/p&gt;

&lt;p&gt;Two follow-ups worth doing this week. Run &lt;code&gt;npm ci --strict-allow-scripts&lt;/code&gt; in a fresh clone on the same OS as your CI runner, so platform-specific optional dependencies surface before they fail a real pipeline. Then check that a package you approved is one you would still approve at its current version, because install scripts are where a compromised release actually executes. For the full npm 12 migration timeline and the 11.16 warning window, see the &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;npm v12 breaking-changes walkthrough&lt;/a&gt;; for the same problem in another ecosystem, &lt;a href="///blog/arrayref-attack-shows-cargo-build-rs-runs-any-code.html"&gt;cargo build.rs runs any code&lt;/a&gt; shows how it plays out in Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I commit &lt;code&gt;package.json&lt;/code&gt; after &lt;code&gt;npm approve-scripts&lt;/code&gt;?&lt;/strong&gt; Yes. The &lt;code&gt;allowScripts&lt;/code&gt; field lives in &lt;code&gt;package.json&lt;/code&gt;, and committing it is the point: the allowlist gets reviewed in a PR and shared by the team. Let the commands maintain the field instead of hand-editing it, but read the diff like any other security change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between &lt;code&gt;--allow-scripts-pending&lt;/code&gt; and approving a package?&lt;/strong&gt; &lt;code&gt;--allow-scripts-pending&lt;/code&gt; is read-only. It lists the packages whose install scripts are not covered by &lt;code&gt;allowScripts&lt;/code&gt; and writes nothing. &lt;code&gt;npm approve-scripts &amp;lt;pkg&amp;gt;&lt;/code&gt; mutates &lt;code&gt;package.json&lt;/code&gt; to allow that package.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does &lt;code&gt;npm approve-scripts&lt;/code&gt; fail on a global install?&lt;/strong&gt; Because it writes to a project &lt;code&gt;package.json&lt;/code&gt; and a global install (&lt;code&gt;-g&lt;/code&gt; or &lt;code&gt;npx&lt;/code&gt;) has none, so it exits &lt;code&gt;EGLOBAL&lt;/code&gt; with "does not work for global installs" (npm/cli #9463). Use &lt;code&gt;npm i -g --allow-scripts=&amp;lt;pkg&amp;gt;&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is &lt;code&gt;--allow-scripts&lt;/code&gt; rejected inside my project?&lt;/strong&gt; npm forbids the install-time flag in project scope and points you at the &lt;code&gt;allowScripts&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt; or at &lt;code&gt;.npmrc&lt;/code&gt;. Inside a repo use &lt;code&gt;npm approve-scripts&lt;/code&gt;; the CLI flag is for global and npx installs only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My local install passed but CI failed on &lt;code&gt;fsevents&lt;/code&gt;. Why?&lt;/strong&gt; &lt;code&gt;--allow-scripts-pending&lt;/code&gt; skips optional and off-platform dependencies, while &lt;code&gt;npm ci --strict-allow-scripts&lt;/code&gt; validates the lockfile and sees them anyway (npm/cli #9562). Approve the optional package explicitly, or run the inventory on the same OS as your CI runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/" rel="noopener noreferrer"&gt;npm-install-scripts, npm Docs (v12)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/npm/cli/issues/9463" rel="noopener noreferrer"&gt;npm/cli issue #9463, approve-scripts fails EGLOBAL on global installs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/npm/cli/issues/9562" rel="noopener noreferrer"&gt;npm/cli issue #9562, strict-allow-scripts rejects an optional package approve-scripts cannot see&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/eslint/eslint/pull/21092" rel="noopener noreferrer"&gt;eslint/eslint PR #21092, a real allowScripts field in package.json&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.blog/changelog/2026-06-09-upcoming-breaking-changes-for-npm-v12/" rel="noopener noreferrer"&gt;Upcoming breaking changes for npm v12, GitHub Changelog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/npm-approve-scripts-vs---allow-scripts-pick-the-right-flag.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>npm allowScripts: the package.json Schema, Not .npmrc</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Sun, 23 Aug 2026 08:32:21 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/npm-allowscripts-the-packagejson-schema-not-npmrc-3e5</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/npm-allowscripts-the-packagejson-schema-not-npmrc-3e5</guid>
      <description>&lt;p&gt;By the end of this you will have a correct &lt;code&gt;allowScripts&lt;/code&gt; block in &lt;code&gt;package.json&lt;/code&gt;, a CI install that fails loudly when a dependency ships an unreviewed install script, and zero &lt;code&gt;allow-scripts&lt;/code&gt; entries left in &lt;code&gt;.npmrc&lt;/code&gt; where they cause a confusing error instead of a policy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; &lt;code&gt;allowScripts&lt;/code&gt; is a plain object in &lt;code&gt;package.json&lt;/code&gt; mapping a dependency to a boolean. &lt;code&gt;true&lt;/code&gt; lets its install script run, &lt;code&gt;false&lt;/code&gt; blocks it. Keys are either pinned (&lt;code&gt;"pkg@1.2.3": true&lt;/code&gt;) or name-only (&lt;code&gt;"pkg": false&lt;/code&gt;). It is a &lt;code&gt;package.json&lt;/code&gt; field. &lt;code&gt;.npmrc&lt;/code&gt; only holds the toggles around it (&lt;code&gt;ignore-scripts&lt;/code&gt;, &lt;code&gt;strict-allow-scripts&lt;/code&gt;, &lt;code&gt;allow-scripts-pin&lt;/code&gt;) plus the &lt;code&gt;allow-scripts&lt;/code&gt; list, which works for global and &lt;code&gt;npx&lt;/code&gt; installs and is rejected in a project install.&lt;/p&gt;

&lt;p&gt;The field shipped in npm 11.16 (commit &lt;a href="https://github.com/npm/cli/commit/a10c7caf3ad9d2d5e17234c5c5e615dbce7717f9" rel="noopener noreferrer"&gt;&lt;code&gt;a10c7ca&lt;/code&gt;&lt;/a&gt;, "Phase 1 of &lt;code&gt;allowScripts&lt;/code&gt; opt-in install-script policy") and becomes enforcing in npm 12, which is late enough that most generated answers conflate the two config surfaces. npm's own error text conflates them too, so the confusion is earned. If you want the migration walkthrough rather than the semantics, read &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;fix the "install scripts not covered" warning before npm 12&lt;/a&gt;; if you already hit the flag error, &lt;a href="///blog/fix-npm---allow-scripts-not-allowed-in-project-installs.html"&gt;Fix npm --allow-scripts not allowed in project installs&lt;/a&gt; covers that one case. This page is the reference both of those point back to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;npm 11.16.0 or newer to see the pending-package warning, npm 12.x to see the default block. Check with &lt;code&gt;npm --version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A project with a &lt;code&gt;package.json&lt;/code&gt;. The field is meaningless without one, and &lt;code&gt;approve-scripts&lt;/code&gt; / &lt;code&gt;deny-scripts&lt;/code&gt; return &lt;code&gt;EGLOBAL&lt;/code&gt; outside a project.&lt;/li&gt;
&lt;li&gt;Knowing which scripts are in scope: &lt;code&gt;preinstall&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;postinstall&lt;/code&gt;, and &lt;code&gt;prepare&lt;/code&gt; for non-registry (git or local) sources. Those are the only lifecycle scripts &lt;code&gt;allowScripts&lt;/code&gt; governs. Your own root-project scripts always run.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Find out which npm you actually have
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 11.16 through 11.x a pending package produces a warning and nothing else. On 12.x the same package gets its script skipped by default. The behavior change is the reason a lockfile that installed fine last quarter can quietly stop running &lt;code&gt;sharp&lt;/code&gt;'s build step after a runner image bump.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. List what is pending, without writing anything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm 12&lt;/span&gt;
npm install-scripts

&lt;span class="c"&gt;# npm 11&lt;/span&gt;
npm approve-scripts &lt;span class="nt"&gt;--allow-scripts-pending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--allow-scripts-pending&lt;/code&gt; is read-only. It prints every package whose install scripts are not yet covered and touches nothing on disk, which is what you want on the first pass so you can review each package before approving it. Do the review here, not later: this list is the only point in the process where you look at what code you are agreeing to execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Write the field, and pick the key shape deliberately
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;allowScripts&lt;/code&gt; is &lt;code&gt;Record&amp;lt;PackageIdentifier, boolean&amp;gt;&lt;/code&gt;. That is the entire schema.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"allowScripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esbuild@0.24.2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@swc/core@1.7.0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sharp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"some-typosquat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two key shapes exist, and choosing between them is the only real decision the field asks of you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key shape&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pinned&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"esbuild@0.24.2": true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Approval is narrowed to that exact version. &lt;code&gt;0.24.3&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; covered and returns to pending.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Name-only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"sharp": true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Applies to any version of the package.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Per the &lt;a href="https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/" rel="noopener noreferrer"&gt;npm-install-scripts docs&lt;/a&gt;, npm writes pinned entries by default, because "By default it writes pinned entries (&lt;code&gt;pkg@1.2.3&lt;/code&gt;), which keep their approval narrowed to the specific version you reviewed." The &lt;code&gt;allow-scripts-pin&lt;/code&gt; config (default &lt;code&gt;true&lt;/code&gt;) controls that; set it to &lt;code&gt;false&lt;/code&gt; if you want generated entries to be name-only.&lt;/p&gt;

&lt;p&gt;My rule: allow pinned, deny name-only. A &lt;code&gt;true&lt;/code&gt; should expire when the code changes, and a &lt;code&gt;false&lt;/code&gt; should not. Writing denials as &lt;code&gt;"some-typosquat": false&lt;/code&gt; means the block survives every version bump, so you never re-review a package you already decided should never execute.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Rule out ignore-scripts before you debug anything else
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm config get ignore-scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns &lt;code&gt;true&lt;/code&gt;, &lt;code&gt;allowScripts&lt;/code&gt; is inert. The docs are explicit that &lt;code&gt;ignore-scripts&lt;/code&gt; "takes precedence and no scripts run, the allowlist does not override it." An &lt;code&gt;.npmrc&lt;/code&gt; at any layer (project, user, global) can set it. Pick one model and stay in it: global-off via &lt;code&gt;ignore-scripts&lt;/code&gt;, or granular via &lt;code&gt;allowScripts&lt;/code&gt;. Running both means the allowlist is decoration.&lt;/p&gt;

&lt;p&gt;Here is the resolution order for a dependency that declares an install script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["Dependency declares an install script"] --&amp;gt; B{"ignore-scripts = true?"}
    B --&amp;gt;|yes| C["No scripts run and allowScripts is ignored"]
    B --&amp;gt;|no| D{"Entry in allowScripts?"}
    D --&amp;gt;|denied| E["Script blocked"]
    D --&amp;gt;|covered| F["Script runs"]
    D --&amp;gt;|pending| G{"strict-allow-scripts set?"}
    G --&amp;gt;|yes| H["Install fails with ESTRICTALLOWSCRIPTS"]
    G --&amp;gt;|no| I["Script skipped, WARN, install succeeds"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three states matter for anything not blanket-disabled by &lt;code&gt;ignore-scripts&lt;/code&gt;. &lt;strong&gt;Covered&lt;/strong&gt; (&lt;code&gt;true&lt;/code&gt;) runs normally. &lt;strong&gt;Denied&lt;/strong&gt; (&lt;code&gt;false&lt;/code&gt;) is blocked, and per the docs "Existing &lt;code&gt;false&lt;/code&gt; entries always win; &lt;code&gt;approve&lt;/code&gt; will not silently re-allow a package you previously denied," which holds even under &lt;code&gt;approve-scripts --all&lt;/code&gt;. &lt;strong&gt;Pending&lt;/strong&gt; (no entry) gets its script skipped with &lt;code&gt;npm warn allow-scripts N packages have install scripts not yet covered by allowScripts&lt;/code&gt;, and the install still exits 0. The &lt;a href="https://github.com/orgs/community/discussions/198547" rel="noopener noreferrer"&gt;community migration discussion&lt;/a&gt; frames it the same way: "an install script you haven't approved gets skipped, you get a warning, and the install still succeeds."&lt;/p&gt;

&lt;p&gt;That exit 0 is the part worth reacting to. A skipped &lt;code&gt;postinstall&lt;/code&gt; on a native module does not fail the install; it fails at runtime, in whatever service imports the module, with a missing binary. The warning scrolls past in CI logs and the build goes green.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Turn pending into a hard failure in CI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# .npmrc
&lt;/span&gt;&lt;span class="py"&gt;strict-allow-scripts&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pending package now fails the install with &lt;code&gt;ESTRICTALLOWSCRIPTS&lt;/code&gt; instead of warning (&lt;a href="https://github.com/npm/cli/issues/9562" rel="noopener noreferrer"&gt;npm/cli #9562&lt;/a&gt;). Set this in CI today, on npm 11, and you get npm 12's enforcement on your schedule rather than on your base image's. It pairs well with &lt;a href="///blog/dependency-cooldowns-beat-fast-supply-chain-attacks.html"&gt;dependency cooldowns&lt;/a&gt;: a new version arrives, it lands as pending, and the pipeline stops until a human looks at it.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Keep the allowlist out of .npmrc
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.npmrc&lt;/code&gt; can carry &lt;code&gt;ignore-scripts&lt;/code&gt;, &lt;code&gt;strict-allow-scripts&lt;/code&gt;, &lt;code&gt;allow-scripts-pin&lt;/code&gt;, and &lt;code&gt;allow-scripts=&amp;lt;comma,separated,names&amp;gt;&lt;/code&gt;. That last one only applies to global and &lt;code&gt;npx&lt;/code&gt; contexts, where there is no &lt;code&gt;package.json&lt;/code&gt; to hold a field:&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;# valid: global install, allow named packages' scripts&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nt"&gt;--allow-scripts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;canvas,sharp some-cli

&lt;span class="c"&gt;# or persist it for global/npx use&lt;/span&gt;
npm config &lt;span class="nb"&gt;set &lt;/span&gt;allow-scripts&lt;span class="o"&gt;=&lt;/span&gt;canvas,sharp &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass &lt;code&gt;--allow-scripts&lt;/code&gt; to a project &lt;code&gt;install&lt;/code&gt;, &lt;code&gt;ci&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, or &lt;code&gt;rebuild&lt;/code&gt; and npm rejects it deliberately: it is "blocked in project installs on purpose (it's for &lt;code&gt;-g&lt;/code&gt; and &lt;code&gt;npx&lt;/code&gt;)."&lt;/p&gt;

&lt;p&gt;The trap, filed as &lt;a href="https://github.com/npm/cli/issues/9783" rel="noopener noreferrer"&gt;npm/cli #9783&lt;/a&gt;, is that a user or global &lt;code&gt;.npmrc&lt;/code&gt; carrying &lt;code&gt;allow-scripts=...&lt;/code&gt; can leak into a project install you never flagged. Preparing a git dependency spawns an inner &lt;code&gt;npm install&lt;/code&gt; and forwards config as &lt;code&gt;npm_config_*&lt;/code&gt; environment variables. The inner install reads &lt;code&gt;allow-scripts&lt;/code&gt; off its env layer, classifies it as a command-line or env policy, and fails with &lt;code&gt;EALLOWSCRIPTS&lt;/code&gt;. The message then tells you to move the entry into &lt;code&gt;.npmrc&lt;/code&gt;, where it already is. Remove &lt;code&gt;allow-scripts&lt;/code&gt; from your user and global &lt;code&gt;.npmrc&lt;/code&gt;, and express project intent through &lt;code&gt;package.json.allowScripts&lt;/code&gt; only.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Commit the field
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"require('./package.json').allowScripts"&lt;/span&gt;
git add package.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: pin allowScripts allowlist"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;allowScripts&lt;/code&gt; is a reviewed-code policy, so it belongs in version control with the same weight as the lockfile. Uncommitted, every teammate and every CI job re-hits the warning and someone eventually approves in a hurry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it works
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm ci &lt;span class="nt"&gt;--strict-allow-scripts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected on success: exit 0 with no &lt;code&gt;allow-scripts&lt;/code&gt; warning, meaning every script-carrying dependency is covered. Expected on failure: a non-zero exit and &lt;code&gt;ESTRICTALLOWSCRIPTS&lt;/code&gt; naming the offending package. Run it once with a deliberately bumped dependency version to confirm the pinned key stops covering it, since that is the behavior most likely to surprise you later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A version bump drops coverage.&lt;/strong&gt; &lt;code&gt;"esbuild@0.24.2": true&lt;/code&gt; does nothing for &lt;code&gt;0.24.3&lt;/code&gt;. The package returns to pending, which is the design, not a bug. Reserve name-only &lt;code&gt;true&lt;/code&gt; keys for packages you trust across every future version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ignore-scripts=true&lt;/code&gt; masks a working allowlist.&lt;/strong&gt; Scripts not running despite a complete &lt;code&gt;allowScripts&lt;/code&gt;? Check &lt;code&gt;npm config get ignore-scripts&lt;/code&gt; first, including the user and global layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--allow-scripts&lt;/code&gt; is not the granular tool.&lt;/strong&gt; It is for &lt;code&gt;-g&lt;/code&gt; and &lt;code&gt;npx&lt;/code&gt;. Copying it out of a global-install tutorial into a project command produces &lt;code&gt;EALLOWSCRIPTS&lt;/code&gt;, and #9783 means you can hit that error without typing the flag at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;approve-scripts&lt;/code&gt; and &lt;code&gt;deny-scripts&lt;/code&gt; fail outside a project&lt;/strong&gt; with &lt;code&gt;EGLOBAL&lt;/code&gt;, because there is no &lt;code&gt;package.json&lt;/code&gt; to write. Use the &lt;code&gt;allow-scripts&lt;/code&gt; config for global and &lt;code&gt;npx&lt;/code&gt; work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A green install is not a working install.&lt;/strong&gt; On npm 12 a pending native module installs cleanly and breaks at import time. If you are already chasing odd install-tree behavior, rule out lockfile damage separately, as in &lt;a href="///blog/fix-npm-cannot-read-properties-of-null-edgesout.html"&gt;Fix npm "Cannot read properties of null (edgesOut)"&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does allowScripts go in package.json or .npmrc?&lt;/strong&gt; The per-package allowlist lives only in &lt;code&gt;package.json&lt;/code&gt;. &lt;code&gt;.npmrc&lt;/code&gt; carries the toggles (&lt;code&gt;ignore-scripts&lt;/code&gt;, &lt;code&gt;strict-allow-scripts&lt;/code&gt;, &lt;code&gt;allow-scripts-pin&lt;/code&gt;) plus the &lt;code&gt;allow-scripts&lt;/code&gt; list for global and &lt;code&gt;npx&lt;/code&gt; installs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between &lt;code&gt;"pkg": true&lt;/code&gt; and &lt;code&gt;"pkg@1.2.3": true&lt;/code&gt;?&lt;/strong&gt; Name-only allows any version. Pinned allows only the version you reviewed, and a new version resets to pending. npm writes pinned entries by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does allowScripts override ignore-scripts?&lt;/strong&gt; No. With &lt;code&gt;ignore-scripts=true&lt;/code&gt;, no scripts run and &lt;code&gt;allowScripts&lt;/code&gt; is ignored entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens to a package that is not listed?&lt;/strong&gt; It is pending: skipped with a warning on npm 12, install still exits 0. With &lt;code&gt;strict-allow-scripts&lt;/code&gt; set, the install fails with &lt;code&gt;ESTRICTALLOWSCRIPTS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do &lt;code&gt;false&lt;/code&gt; entries ever get re-enabled automatically?&lt;/strong&gt; No. An existing &lt;code&gt;false&lt;/code&gt; wins, and &lt;code&gt;approve-scripts --all&lt;/code&gt; will not undo it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;The model in one line: &lt;code&gt;allowScripts&lt;/code&gt; is &lt;code&gt;Record&amp;lt;PackageIdentifier, boolean&amp;gt;&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;, pinned by default, &lt;code&gt;false&lt;/code&gt; always wins, and &lt;code&gt;ignore-scripts&lt;/code&gt; short-circuits all of it. The per-package allowlist never belongs in &lt;code&gt;.npmrc&lt;/code&gt;; that file holds the toggles and the global-only &lt;code&gt;allow-scripts&lt;/code&gt; list.&lt;/p&gt;

&lt;p&gt;Do these in order. Run &lt;code&gt;npm install-scripts&lt;/code&gt; (or &lt;code&gt;approve-scripts --allow-scripts-pending&lt;/code&gt; on 11.x) and read the list before approving anything. Write approvals pinned and denials name-only. Set &lt;code&gt;strict-allow-scripts=true&lt;/code&gt; in CI so an unreviewed script fails the build now instead of after your runner picks up npm 12. Grep your user and global &lt;code&gt;.npmrc&lt;/code&gt; for &lt;code&gt;allow-scripts&lt;/code&gt; and delete it. Commit &lt;code&gt;package.json&lt;/code&gt;. The reason to bother is the same reason &lt;a href="///blog/arrayref-attack-shows-cargo-build-rs-runs-any-code.html"&gt;&lt;code&gt;build.rs&lt;/code&gt; in Cargo deserves an allowlist it does not have&lt;/a&gt;: an install script executes with your developer's credentials and your runner's network access, before any code you wrote has run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.npmjs.com/cli/v12/commands/npm-install-scripts/" rel="noopener noreferrer"&gt;npm-install-scripts | npm Docs (v12)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/orgs/community/discussions/198547" rel="noopener noreferrer"&gt;Preparing for npm v12: install scripts become opt-in, community Discussion #198547&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/npm/cli/issues/9783" rel="noopener noreferrer"&gt;npm/cli #9783, user/global .npmrc allow-scripts leaks into git-dependency prep&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/npm/cli/commit/a10c7caf3ad9d2d5e17234c5c5e615dbce7717f9" rel="noopener noreferrer"&gt;npm/cli commit a10c7ca, Phase 1 of allowScripts opt-in policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/npm/cli/issues/9562" rel="noopener noreferrer"&gt;npm/cli #9562, strict-allow-scripts behavior&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/npm-allowscripts-the-package-json-schema-not-npmrc.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>arrayref Attack Shows cargo build.rs Runs Any Code</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Fri, 21 Aug 2026 03:35:17 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/arrayref-attack-shows-cargo-buildrs-runs-any-code-2hi7</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/arrayref-attack-shows-cargo-buildrs-runs-any-code-2hi7</guid>
      <description>&lt;p&gt;On August 20, 2026, someone published &lt;code&gt;arrayref@0.3.10&lt;/code&gt; to crates.io at 07:15 UTC. It was gone by 08:41, alive for 86 minutes. During that window nobody had to &lt;em&gt;call&lt;/em&gt; the crate. Resolving it into your dependency tree and running &lt;code&gt;cargo build&lt;/code&gt; was enough to download and execute a remote binary with your user privileges.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; the malicious code shipped inside a build script (&lt;code&gt;build.rs&lt;/code&gt;) carried by &lt;code&gt;proc-macro1&lt;/code&gt;, a one-character typosquat of &lt;code&gt;proc-macro2&lt;/code&gt; that &lt;code&gt;arrayref@0.3.10&lt;/code&gt; added as a dependency. Cargo runs &lt;code&gt;build.rs&lt;/code&gt; automatically at compile time, so &lt;code&gt;cargo build&lt;/code&gt; or &lt;code&gt;cargo update&lt;/code&gt; triggered it with no function call required. Rust has no &lt;code&gt;cargo --ignore-scripts&lt;/code&gt; equivalent, so the fix is not a flag. It is committing &lt;code&gt;Cargo.lock&lt;/code&gt;, building with &lt;code&gt;--locked&lt;/code&gt;, and refusing brand-new versions with a cooldown.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;arrayref&lt;/code&gt; is a nine-line convenience crate with roughly 245 million downloads, sitting under &lt;code&gt;winit&lt;/code&gt;, &lt;code&gt;egui&lt;/code&gt;, &lt;code&gt;iced&lt;/code&gt;, &lt;code&gt;blake3&lt;/code&gt;, and a swath of Solana and Ethereum tooling, per The Hacker News and the Rust Security Response Team. The poisoned release added one new dependency: &lt;code&gt;proc-macro1&lt;/code&gt;, published from &lt;code&gt;dtolney&lt;/code&gt;, a near-miss of the real maintainer account &lt;code&gt;dtolnay&lt;/code&gt;. Two sibling crates from the same owner, &lt;code&gt;internment@0.8.7&lt;/code&gt; and &lt;code&gt;append-only-vec@0.1.9&lt;/code&gt;, were poisoned the same morning and pulled within 90 and 107 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually executed during cargo build?
&lt;/h2&gt;

&lt;p&gt;The payload ran at build time, not run time. Per StepSecurity's teardown and the Rust Project advisory, the malicious logic lived in &lt;code&gt;proc-macro1&lt;/code&gt;'s &lt;code&gt;build.rs&lt;/code&gt;. Nothing from &lt;code&gt;arrayref&lt;/code&gt; or its siblings had to be invoked. Resolving the tree and compiling it was the trigger.&lt;/p&gt;

&lt;p&gt;The script was written to slip past a casual look. It reconstructed its C2 host (&lt;code&gt;23.254.165.112&lt;/code&gt;, a Hostwinds VPS) from base64 URL fragments, used an &lt;code&gt;AcceptAll&lt;/code&gt; TLS verifier so it could talk to a bare IP with no valid certificate, wrote &lt;code&gt;/tmp/rust-setup&lt;/code&gt; on Unix (a &lt;code&gt;.ps1&lt;/code&gt; plus &lt;code&gt;.vbs&lt;/code&gt; pair driven by &lt;code&gt;wscript.exe&lt;/code&gt; on Windows), and called &lt;code&gt;std::mem::forget(child)&lt;/code&gt; so the spawned process outlived the build. On one Linux victim, stage two dropped &lt;code&gt;MonoService&lt;/code&gt; and &lt;code&gt;MonoXpc&lt;/code&gt; binaries under &lt;code&gt;~/.config/AzureKits&lt;/code&gt; and registered a systemd service for persistence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A["cargo build or cargo update"] --&amp;gt; B["Resolve dependency tree"]
  B --&amp;gt; C["arrayref 0.3.10 pulls proc-macro1"]
  C --&amp;gt; D{"proc-macro1 ships a build.rs?"}
  D --&amp;gt;|yes| E["Cargo runs build.rs at compile time"]
  E --&amp;gt; F["Rebuild C2 host from base64 fragments"]
  F --&amp;gt; G["Fetch binary over TLS with AcceptAll verifier"]
  G --&amp;gt; H["Spawn /tmp/rust-setup, std mem forget child"]
  H --&amp;gt; I["Stage two: AzureKits binaries plus systemd persistence"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trust boundary that matters here is not "code I wrote" versus "libraries I use." It is "code that runs when I compile." In Rust those are different sets, and &lt;code&gt;build.rs&lt;/code&gt; is the second one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "I never called that crate" is the wrong model
&lt;/h2&gt;

&lt;p&gt;Every JavaScript developer reading this already survived the install-script era. npm shipped &lt;code&gt;--ignore-scripts&lt;/code&gt;, then npm 12 turned install scripts &lt;em&gt;off by default&lt;/em&gt; behind an allowlist you commit to &lt;code&gt;package.json&lt;/code&gt;, and pnpm added its own &lt;code&gt;onlyBuiltDependencies&lt;/code&gt; gate. If you have wrestled with the &lt;a href="///blog/fix-npm---allow-scripts-not-allowed-in-project-installs.html"&gt;&lt;code&gt;npm --allow-scripts not allowed in project installs&lt;/code&gt; error&lt;/a&gt; or the &lt;a href="///blog/npm-v12-breaking-changes-lock-down-install-scripts.html"&gt;npm 12 allowScripts warning&lt;/a&gt;, you already know the muscle: name the packages allowed to run code at install, deny the rest.&lt;/p&gt;

&lt;p&gt;Rust's &lt;code&gt;build.rs&lt;/code&gt; is the structural twin of an npm install script, with one difference. Rust never shipped the seatbelt. There is no flag today that says "compile this project, but do not execute my dependencies' build scripts." So the assumption a JavaScript team now gets for free is unavailable to a Rust team in August 2026, and this incident is the first at-scale demonstration of what that gap costs.&lt;/p&gt;

&lt;p&gt;The second-order effect is where teams get burned. Because the payload sits in a build script, static reasoning about your own code (grepping for &lt;code&gt;use arrayref&lt;/code&gt;, checking which functions you call) tells you nothing. A dependency you pulled purely as a transitive requirement of &lt;code&gt;winit&lt;/code&gt; or &lt;code&gt;blake3&lt;/code&gt;, and never reference directly, ran code on your build box. Audit the set of crates in your resolved graph that carry a &lt;code&gt;build.rs&lt;/code&gt;, not the set you import.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is &lt;code&gt;--offline&lt;/code&gt; the fix? No, and here is why
&lt;/h2&gt;

&lt;p&gt;The reflex I saw within hours of the disclosure was "I'll just build &lt;code&gt;--offline&lt;/code&gt;." That is false comfort. Offline builds only protect you if the malicious &lt;code&gt;.crate&lt;/code&gt; is not already sitting in &lt;code&gt;~/.cargo/registry/cache&lt;/code&gt;. Once a poisoned &lt;code&gt;cargo update&lt;/code&gt; has run, it is there, and an offline build will happily compile and execute it.&lt;/p&gt;

&lt;p&gt;The controls that would have shrunk this to zero are duller and older. Commit &lt;code&gt;Cargo.lock&lt;/code&gt; and build with &lt;code&gt;--locked&lt;/code&gt; so CI never silently resolves a brand-new version behind your back. Then add a cooldown that refuses versions younger than a few days. An 86-minute exposure window does not survive a 3-day cooldown, because no build in that window ever sees the poisoned version as installable. This is the same argument the &lt;a href="///blog/dependency-cooldowns-beat-fast-supply-chain-attacks.html"&gt;dependency cooldown&lt;/a&gt; work makes on the npm side, and it ports directly to Cargo.&lt;/p&gt;

&lt;p&gt;One more trap: yanking the version upstream does not clean your machine. The Rust advisory ships a &lt;code&gt;find&lt;/code&gt; command against &lt;code&gt;~/.cargo/registry/cache&lt;/code&gt; for exactly this reason. Cached &lt;code&gt;.crate&lt;/code&gt; files survive an upstream yank. Any host that ran &lt;code&gt;cargo build&lt;/code&gt; or &lt;code&gt;cargo update&lt;/code&gt; in the 07:11 to 09:25 UTC window on August 20 should be treated as compromised, with its reachable credentials rotated.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a one-character typosquat survived a glance
&lt;/h2&gt;

&lt;p&gt;The attacker staged the name to look legitimate. They first published a clean &lt;code&gt;proc-macro1@1.0.106&lt;/code&gt;, a bit-for-bit copy of &lt;code&gt;proc-macro2&lt;/code&gt; source with no payload, to build innocent history. Then they weaponized &lt;code&gt;1.0.107&lt;/code&gt; with the malicious &lt;code&gt;build.rs&lt;/code&gt; and three build-only dependencies (&lt;code&gt;base64&lt;/code&gt;, &lt;code&gt;rustls&lt;/code&gt;, &lt;code&gt;ureq&lt;/code&gt;), per StepSecurity. A developer who clicked through to vet the crate would have found a real, building, plausible package with prior releases. Provenance-by-eyeballing loses against this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cargo vet&lt;/code&gt; and &lt;code&gt;cargo-deny&lt;/code&gt; are the durable answer, and it is worth being honest that neither would have flagged &lt;code&gt;1.0.107&lt;/code&gt; the instant it landed. What &lt;code&gt;cargo vet&lt;/code&gt; gives you is a "who audited this, and this exact version" model, which is the thing typosquat-by-history is designed to defeat. That is a different job from a CVE scanner. If you want the distinction between scan-what-exists and attest-what-you-trust laid out for the container world, the same split shows up in &lt;a href="///blog/trivy-vs-grype-2026-pick-by-the-job-not-speed.html"&gt;Trivy versus Grype&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who ran this, and why the target set fits
&lt;/h2&gt;

&lt;p&gt;Wiz reports significant overlap between this campaign's infrastructure and tradecraft and prior DPRK-linked operations. That reading is consistent with the stage-two persistence, which focused on credentials and crypto tooling, and with the choice of crates sitting under Solana and Ethereum ecosystems. The counterpoint worth stating: attribution from infrastructure reuse is a probabilistic claim, not proof, and the defensive posture does not change based on who did it. Whether the actor is a nation-state or a bored opportunist, the control gap (build scripts run arbitrary code, and Cargo cannot stop them) is identical.&lt;/p&gt;

&lt;p&gt;The broader signal is the cadence. Published-then-yanked-in-under-two-hours is now a repeatable pattern across npm, PyPI, and Cargo. Speed is the attacker's advantage, and every defense that depends on a human noticing in time is already beaten.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do before your next CI build
&lt;/h2&gt;

&lt;p&gt;Order these by what bites first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Commit &lt;code&gt;Cargo.lock&lt;/code&gt; and add &lt;code&gt;--locked&lt;/code&gt; to every CI build command today.&lt;/strong&gt; For applications there is no excuse: &lt;code&gt;cargo build --locked&lt;/code&gt; fails the build rather than silently resolving &lt;code&gt;0.3.10&lt;/code&gt;. This one line closes the exact vector that pulled the poisoned version in during the 86-minute window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a cooldown of 3 to 7 days on new crate versions&lt;/strong&gt; via &lt;code&gt;cargo-deny&lt;/code&gt; policy or a filtering mirror. Every published-then-yanked-in-90-minutes attack dies against a 3-day floor. Pick 3 days as a minimum; go to 7 if your release pace allows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan your cache for the known-bad versions now.&lt;/strong&gt; Run the advisory's &lt;code&gt;find&lt;/code&gt; against &lt;code&gt;~/.cargo/registry/cache&lt;/code&gt; for &lt;code&gt;arrayref@0.3.10&lt;/code&gt;, &lt;code&gt;internment@0.8.7&lt;/code&gt;, &lt;code&gt;proc-macro1&lt;/code&gt; (any version), and &lt;code&gt;append-only-vec@0.1.9&lt;/code&gt;. If any host ran &lt;code&gt;cargo build&lt;/code&gt; or &lt;code&gt;cargo update&lt;/code&gt; between 07:11 and 09:25 UTC on Aug 20, rotate every credential reachable from that host, including cloud tokens and signing keys. Check &lt;code&gt;~/.config/AzureKits&lt;/code&gt; and for a rogue systemd service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baseline build-time egress, not just runtime.&lt;/strong&gt; StepSecurity's Harden-Runner caught this because a build phoning &lt;code&gt;23.254.165.112:9089&lt;/code&gt; had never appeared in the workflow's network baseline. If your CI cannot see what &lt;code&gt;cargo build&lt;/code&gt; talks to, wire up egress monitoring; the same &lt;a href="///blog/kubernetes-default-deny-egress-stops-pod-exfiltration.html"&gt;default-deny egress&lt;/a&gt; logic that contains a pod contains a build runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt &lt;code&gt;cargo vet&lt;/code&gt; for your critical dependency set&lt;/strong&gt; and pin trusted publishers. It will not catch a fresh typosquat instantly, but it makes version-level provenance a gate instead of a hope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track cargo #13681 (build-script allowlist) and #5720 (sandbox).&lt;/strong&gt; Until one ships, treat every &lt;code&gt;cargo build&lt;/code&gt; on a credentialed machine as arbitrary code execution by your entire transitive tree, because that is what it is.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does building with &lt;code&gt;--offline&lt;/code&gt; protect me?&lt;/strong&gt; Only if the poisoned &lt;code&gt;.crate&lt;/code&gt; never reached your cache. After a &lt;code&gt;cargo update&lt;/code&gt; pulled it, the file is in &lt;code&gt;~/.cargo/registry/cache&lt;/code&gt; and offline builds still run its &lt;code&gt;build.rs&lt;/code&gt;. Use &lt;code&gt;--locked&lt;/code&gt; against a committed lockfile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I never called &lt;code&gt;arrayref&lt;/code&gt;. Am I safe?&lt;/strong&gt; No. The payload was in a &lt;code&gt;build.rs&lt;/code&gt;, which Cargo runs for any crate in the resolved graph regardless of whether you import it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is there a &lt;code&gt;cargo --ignore-scripts&lt;/code&gt;?&lt;/strong&gt; No. As of August 2026 no flag skips dependency build scripts. Proposals cargo #13681 and #5720 are open and unmerged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I know if I was hit?&lt;/strong&gt; Run the advisory's &lt;code&gt;find&lt;/code&gt; over &lt;code&gt;~/.cargo/registry/cache&lt;/code&gt;, and treat any host that compiled between 07:11 and 09:25 UTC on Aug 20 as compromised. Rotate its credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on-arrayref/" rel="noopener noreferrer"&gt;https://blog.rust-lang.org/2026/08/20/supply-chain-attack-on-arrayref/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rustsec/advisory-db/issues/3161" rel="noopener noreferrer"&gt;https://github.com/rustsec/advisory-db/issues/3161&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.stepsecurity.io/blog/arrayref-rust-crate-supply-chain-attack" rel="noopener noreferrer"&gt;https://www.stepsecurity.io/blog/arrayref-rust-crate-supply-chain-attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rust-lang/cargo/issues/13681" rel="noopener noreferrer"&gt;https://github.com/rust-lang/cargo/issues/13681&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rust-lang/cargo/issues/5720" rel="noopener noreferrer"&gt;https://github.com/rust-lang/cargo/issues/5720&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/arrayref-attack-shows-cargo-build-rs-runs-any-code.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
    </item>
    <item>
      <title>Fix "unrecognized format int32" in Kubernetes 1.34</title>
      <dc:creator>Indra Gusti Prasetya</dc:creator>
      <pubDate>Tue, 18 Aug 2026 04:43:14 +0000</pubDate>
      <link>https://dev.to/indra_gustiprasetya_a80a/fix-unrecognized-format-int32-in-kubernetes-134-531</link>
      <guid>https://dev.to/indra_gustiprasetya_a80a/fix-unrecognized-format-int32-in-kubernetes-134-531</guid>
      <description>&lt;p&gt;After upgrading a cluster to Kubernetes 1.34.0, &lt;code&gt;kubectl apply&lt;/code&gt; and &lt;code&gt;helm install&lt;/code&gt; start printing &lt;code&gt;Warning: unrecognized format "int32"&lt;/code&gt; (and &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;) against CRDs that were valid the day before. Your CRDs are fine. This guide shows how to prove the warning is a kube-apiserver regression, tell it apart from a real format typo, and either patch the control plane or filter the noise cleanly until you can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short version:&lt;/strong&gt; the warning is a server-side regression in kube-apiserver 1.34.0 that wrongly applied a new unknown-format check to &lt;code&gt;integer&lt;/code&gt; and &lt;code&gt;number&lt;/code&gt; properties. It was fixed in PR &lt;a href="https://github.com/kubernetes/kubernetes/pull/133896" rel="noopener noreferrer"&gt;#133896&lt;/a&gt;, cherry-picked to the 1.34 patch line on 2025-09-05, and it ships clean in 1.35.0. The fix is a control-plane upgrade, not a kubectl or CRD change.&lt;/p&gt;

&lt;p&gt;This is for platform and cluster operators who suddenly see a wall of format warnings from kube-prometheus-stack, cert-manager, Cilium, or their own operators, and have to decide whether to act. The symptom is all over the issue trackers, and almost every thread misattributes the cause: people get told to edit their CRD schemas or downgrade kubectl. Both are wrong. The warning is emitted by the API server, not the client, and the schemas were correct all along. Below is the one command that proves where the warning originates, so you can separate this false positive from a genuine format typo. If you are mid-rollout, file it next to the other 1.34/1.35 surprises like &lt;a href="///blog/kubernetes-user-namespaces-in-1-36-with-hostusers-false.html"&gt;user namespaces going GA&lt;/a&gt;, and treat it the same way: it is a new server version changing behavior underneath stable tooling, the same shape of break as &lt;a href="///blog/cosign-v3-sign-and-verify-images-fix-harbor-breaks.html"&gt;Cosign v3 breaking image verification in Harbor&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A cluster running &lt;strong&gt;Kubernetes 1.34.0&lt;/strong&gt; (control plane). Check with &lt;code&gt;kubectl version&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; v1.28+ with server-side field validation (the default since 1.27).&lt;/li&gt;
&lt;li&gt;Cluster-admin or the ability to apply a CRD in a scratch namespace.&lt;/li&gt;
&lt;li&gt;If you run &lt;strong&gt;EKS, GKE, or AKS&lt;/strong&gt;, note your provider's control-plane patch version. You likely cannot bump it yourself, which changes your options (see pitfalls).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Reproduce the warning with a minimal CRD
&lt;/h3&gt;

&lt;p&gt;Apply a CRD that declares an &lt;code&gt;integer&lt;/code&gt; property with &lt;code&gt;format: int32&lt;/code&gt;, the exact shape kube-prometheus-stack and cert-manager ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# int32-crd.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apiextensions.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CustomResourceDefinition&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;widgets.example.com&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;example.com&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Namespaced&lt;/span&gt;
  &lt;span class="na"&gt;names&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plural&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;widgets&lt;/span&gt;
    &lt;span class="na"&gt;singular&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;widget&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Widget&lt;/span&gt;
  &lt;span class="na"&gt;versions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
      &lt;span class="na"&gt;served&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;openAPIV3Schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
          &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
              &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integer&lt;/span&gt;
                  &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;int32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; int32-crd.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 1.34.0 you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Warning: unrecognized format "int32"
customresourcedefinition.apiextensions.k8s.io/widgets.example.com created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resource is still created. The warning is non-blocking, but in a real chart it repeats dozens of times and buries anything that actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Confirm the warning is server-side, not from kubectl
&lt;/h3&gt;

&lt;p&gt;This is the step everyone skips. Add &lt;code&gt;-v=8&lt;/code&gt; and watch for a &lt;code&gt;Warning&lt;/code&gt; &lt;strong&gt;response header&lt;/strong&gt; from the API server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; int32-crd.yaml &lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; warning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the warning arrive as an HTTP response header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Response Headers:
    Warning: 299 - "unrecognized format \"int32\""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;299&lt;/code&gt; warning header is generated by kube-apiserver, not printed locally by kubectl. That single line tells you upgrading or downgrading the CLI will do nothing, because the behavior lives in the control plane. This matches the 1.35 release note, which files the fix under "Kube-apiserver."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Confirm it's a false positive, not a real typo
&lt;/h3&gt;

&lt;p&gt;The check itself is legitimate: it exists to catch format typos like &lt;code&gt;format: itn32&lt;/code&gt;. The bug is that 1.34.0 applied it to non-string types. Per the &lt;a href="https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/" rel="noopener noreferrer"&gt;CRD documentation&lt;/a&gt;, &lt;code&gt;int32&lt;/code&gt;, &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, and &lt;code&gt;double&lt;/code&gt; are documented, supported formats, so a warning on any of those four attached to a &lt;code&gt;type: integer&lt;/code&gt; or &lt;code&gt;type: number&lt;/code&gt; field is the regression, not a mistake. Issue &lt;a href="https://github.com/kubernetes/kubernetes/issues/133880" rel="noopener noreferrer"&gt;#133880&lt;/a&gt; is the upstream confirmation.&lt;/p&gt;

&lt;p&gt;A rule of thumb you can apply while scanning output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warning on &lt;code&gt;int32&lt;/code&gt;/&lt;code&gt;int64&lt;/code&gt;/&lt;code&gt;float&lt;/code&gt;/&lt;code&gt;double&lt;/code&gt; under &lt;code&gt;type: integer&lt;/code&gt;/&lt;code&gt;number&lt;/code&gt; is a &lt;strong&gt;false positive&lt;/strong&gt;, ignore it.&lt;/li&gt;
&lt;li&gt;Warning on a &lt;code&gt;type: string&lt;/code&gt; field with a genuinely unknown format (&lt;code&gt;format: emial&lt;/code&gt;) is &lt;strong&gt;real&lt;/strong&gt;, fix your schema.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Apply the real fix: patch the control plane
&lt;/h3&gt;

&lt;p&gt;The fix (PR &lt;a href="https://github.com/kubernetes/kubernetes/pull/133896" rel="noopener noreferrer"&gt;#133896&lt;/a&gt;) restricts the unknown-format warning to &lt;code&gt;type: string&lt;/code&gt; properties, so integer and number formats stop warning. It merged to &lt;code&gt;master&lt;/code&gt; under the v1.35 milestone and was cherry-picked to &lt;code&gt;release-1.34&lt;/code&gt; (PR #133901, merged 2025-09-05).&lt;/p&gt;

&lt;p&gt;On a self-managed cluster, upgrade the control plane to a 1.34 patch that includes the cherry-pick, or to 1.35.0+:&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;# self-managed / kubeadm example, upgrade the control-plane node&lt;/span&gt;
kubeadm upgrade plan
kubeadm upgrade apply v1.35.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the served version moved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl version &lt;span class="nt"&gt;-o&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.serverVersion.gitVersion'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. If you can't upgrade yet, filter, don't rewrite
&lt;/h3&gt;

&lt;p&gt;On a managed cluster you are stuck on the provider's control-plane version until they roll the patch. Do not edit vendored CRDs to drop &lt;code&gt;format: int32&lt;/code&gt;; you will silently weaken validation and create drift you have to unwind later. Filter the noise at the point of apply so real warnings still surface:&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;# keep every line EXCEPT the four known false positives&lt;/span&gt;
helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; monitoring prometheus-community/kube-prometheus-stack &lt;span class="se"&gt;\&lt;/span&gt;
  2&amp;gt; &lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'unrecognized format "(int32|int64|float|double)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This suppresses only the four documented-format false positives and lets a genuine &lt;code&gt;type: string&lt;/code&gt; typo through untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it works
&lt;/h2&gt;

&lt;p&gt;Re-apply the same CRD after the control plane is patched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; int32-crd.yaml &lt;span class="nt"&gt;-v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'unrecognized format'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"clean"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output on a fixed control plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;299&lt;/code&gt; header, no terminal warning. If you still see it, your &lt;code&gt;serverVersion&lt;/code&gt; from step 4 has not actually moved, and the client version is irrelevant here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the warning comes from
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
    A["kubectl apply -f crd.yaml"] --&amp;gt; B["kube-apiserver\nvalidates CRD OpenAPI schema"]
    B --&amp;gt; C{"format on a\nstring property?"}
    C --&amp;gt;|"string, unknown format\n(real typo)"| D["Warning: 299\nunrecognized format"]
    C --&amp;gt;|"integer/number in 1.34.0\n(the regression)"| D
    C --&amp;gt;|"integer/number on\npatched apiserver"| E["no warning"]
    D --&amp;gt; F["kubectl prints the\nserver Warning header"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common pitfalls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Just update kubectl."&lt;/strong&gt; The warning is a server response header (step 2). A newer or older CLI changes nothing. This is the single most common wrong answer in the wild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editing the operator's CRDs to remove &lt;code&gt;format: int32&lt;/code&gt;.&lt;/strong&gt; You are patching a false positive by degrading real schema validation, and your change gets clobbered on the next chart upgrade. Filter at apply time instead (step 5).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming every warning in the batch is the same bug.&lt;/strong&gt; On 1.34, kube-prometheus-stack installs also emit &lt;code&gt;Warning: spec.SessionAffinity is ignored for headless services&lt;/code&gt; (&lt;a href="https://github.com/prometheus-community/helm-charts/issues/6108" rel="noopener noreferrer"&gt;issue #6108&lt;/a&gt;). That one is unrelated to formats: it flags a &lt;code&gt;sessionAffinity&lt;/code&gt; set on a &lt;code&gt;clusterIP: None&lt;/code&gt; Service and is a real, if harmless, config smell. Only the four numeric-format warnings are the regression.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI gates that fail on any stderr.&lt;/strong&gt; If your pipeline treats warning output as failure, a control-plane bump you don't control (managed cluster auto-patch) can flip a job red or green with no change on your side. Pin the filter from step 5 into the pipeline until you are on a fixed control plane, then remove it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chasing it as a Cilium or cert-manager bug.&lt;/strong&gt; Both projects got issues filed against them (&lt;a href="https://github.com/cilium/cilium/issues/41826" rel="noopener noreferrer"&gt;Cilium #41826&lt;/a&gt;) for warnings that were never theirs to fix. If the format is one of the four numeric ones, close it and point at the apiserver.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is &lt;code&gt;Warning: unrecognized format "int32"&lt;/code&gt; breaking my CRD install?&lt;/strong&gt;&lt;br&gt;
No. It's a non-blocking warning; the CRD is still created and validated normally. The only cost is noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which Kubernetes version fixes it?&lt;/strong&gt;&lt;br&gt;
kube-apiserver 1.35.0 ships the fix, and it was cherry-picked to the 1.34 patch line (PR #133896 / #133901, merged 2025-09-05). Upgrade the control plane, not kubectl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to change my CRD YAML?&lt;/strong&gt;&lt;br&gt;
No. &lt;code&gt;int32&lt;/code&gt;, &lt;code&gt;int64&lt;/code&gt;, &lt;code&gt;float&lt;/code&gt;, and &lt;code&gt;double&lt;/code&gt; are documented, supported formats. Removing them only weakens validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm on EKS/GKE/AKS and can't pick the patch, what do I do?&lt;/strong&gt;&lt;br&gt;
Filter the four false-positive strings at apply time (step 5) and wait for your provider to roll a fixed 1.34 patch. Confirm with &lt;code&gt;kubectl version -o json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I tell this apart from a real format typo?&lt;/strong&gt;&lt;br&gt;
Real typos warn on &lt;code&gt;type: string&lt;/code&gt; fields with an unknown format. The regression warns on &lt;code&gt;type: integer&lt;/code&gt;/&lt;code&gt;number&lt;/code&gt; fields using one of the four numeric formats. Check the field's &lt;code&gt;type&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Warning: unrecognized format "int32"&lt;/code&gt; on Kubernetes 1.34 is a kube-apiserver false positive. Prove it's server-side with &lt;code&gt;-v=8&lt;/code&gt;, separate it from a genuine format typo and from the unrelated &lt;code&gt;SessionAffinity&lt;/code&gt; warning, then either patch the control plane to a fixed 1.34 point release (or 1.35.0+) or filter the four numeric formats until you can. Fold it into your 1.34/1.35 upgrade checklist alongside other version-pinned gotchas, including the &lt;a href="///blog/migrate-ingress-nginx-to-gateway-api-before-retirement.html"&gt;Ingress-NGINX to Gateway API migration&lt;/a&gt;, where the same trap bites: the docs still describe the pre-change behavior. The discipline that saves you on all of them is the same, check the served version, not the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes issue #133880, CRD validation warning on valid formats: &lt;a href="https://github.com/kubernetes/kubernetes/issues/133880" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kubernetes/issues/133880&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kubernetes PR #133896, restrict unknown-format warning to &lt;code&gt;type=string&lt;/code&gt;: &lt;a href="https://github.com/kubernetes/kubernetes/pull/133896" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kubernetes/pull/133896&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;prometheus-community/helm-charts issue #6108 to 1.34 warnings in kube-prometheus-stack: &lt;a href="https://github.com/prometheus-community/helm-charts/issues/6108" rel="noopener noreferrer"&gt;https://github.com/prometheus-community/helm-charts/issues/6108&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;cilium/cilium issue #41826, unrecognized format int32/int64 on helm install: &lt;a href="https://github.com/cilium/cilium/issues/41826" rel="noopener noreferrer"&gt;https://github.com/cilium/cilium/issues/41826&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Kubernetes v1.34 release announcement (2025-08-27): &lt;a href="https://kubernetes.io/blog/2025/08/27/kubernetes-v1-34-release/" rel="noopener noreferrer"&gt;https://kubernetes.io/blog/2025/08/27/kubernetes-v1-34-release/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://indragustiprasetya.com/blog/fix-unrecognized-format-int32-in-kubernetes-1-34.html?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=article" rel="noopener noreferrer"&gt;indragustiprasetya.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
