<?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: Ilan Kim</title>
    <description>The latest articles on DEV Community by Ilan Kim (@ilan_kim).</description>
    <link>https://dev.to/ilan_kim</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%2F4120289%2Faf6b8a4b-75f0-46f4-8d4e-7e1d5e1b540a.JPG</url>
      <title>DEV Community: Ilan Kim</title>
      <link>https://dev.to/ilan_kim</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ilan_kim"/>
    <language>en</language>
    <item>
      <title>sshole CVE-2026-123158 on Alpine: ssh -V can't tell you if it's patched, so check the package revision and restart sshd</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Fri, 06 Nov 2026 01:41:44 +0000</pubDate>
      <link>https://dev.to/ilan_kim/sshole-cve-2026-123158-on-alpine-ssh-v-cant-tell-you-if-its-patched-so-check-the-package-3ln0</link>
      <guid>https://dev.to/ilan_kim/sshole-cve-2026-123158-on-alpine-ssh-v-cant-tell-you-if-its-patched-so-check-the-package-3ln0</guid>
      <description>&lt;p&gt;On Alpine's backported stable branches, &lt;code&gt;ssh -V&lt;/code&gt; cannot tell you whether CVE-2026-123158 (sshole) is patched: on 3.22, the vulnerable &lt;code&gt;10.0_p1-r10&lt;/code&gt; and the fixed &lt;code&gt;10.0_p1-r11&lt;/code&gt; both print &lt;code&gt;OpenSSH_10.0p2&lt;/code&gt;. Compare the package revision against the Alpine advisory instead, then confirm that the running sshd was actually restarted.&lt;/p&gt;

&lt;p&gt;The issue, nicknamed sshole, was disclosed on 2026-11-05 alongside OpenSSH 10.6p2 and Alpine's patches. Plenty of coverage already explains the bug, so this post sticks to the judgment call: is my own sshd on musl exposed, patched, and running the patched binary? I don't run sshd on musl or Alpine myself, so I only verified the check procedure in clean containers on 2026-11-06. I did not reproduce the bypass or attempt any login. A read-only POSIX shell script is included below.&lt;/p&gt;

&lt;h2&gt;
  
  
  All four conditions must hold for CVE-2026-123158 to apply
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://alpinelinux.org/posts/2026-11-05-openssh-password-authentication-bypass/" rel="noopener noreferrer"&gt;Alpine advisory&lt;/a&gt; lists four conditions. &lt;strong&gt;If any one is missing, the bypass does not work.&lt;/strong&gt; The measured column shows the default state of containers with the fixed package on Alpine 3.22.8, 3.23.8 and 3.24.4.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;How to check&lt;/th&gt;
&lt;th&gt;Measured on a default install&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;sshd without PAM&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;usepam&lt;/code&gt; line in &lt;code&gt;sshd -T&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no such line (build without PAM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;PasswordAuthentication&lt;/code&gt; is on&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sshd -T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An account whose password field is &lt;code&gt;*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;second field of &lt;code&gt;/etc/shadow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;root&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Another local account with a &lt;code&gt;$5$&lt;/code&gt; or &lt;code&gt;$6$&lt;/code&gt; hash&lt;/td&gt;
&lt;td&gt;second field of &lt;code&gt;/etc/shadow&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a fresh install lacks the fourth condition. The moment someone sets one local password, all four are met. A server with key-only &lt;code&gt;*&lt;/code&gt; accounts next to a password account fits this.&lt;/p&gt;

&lt;p&gt;The advisory also counts directory users coming through musl-nscd under the third condition. They have no local shadow entry, so &lt;code&gt;/etc/shadow&lt;/code&gt; will not show them.&lt;/p&gt;

&lt;p&gt;The root cause sits outside OpenSSH, as the &lt;a href="https://www.openssh.org/releasenotes.html" rel="noopener noreferrer"&gt;OpenSSH release notes&lt;/a&gt; say. According to the advisory, musl's &lt;code&gt;crypt()&lt;/code&gt; returns &lt;code&gt;*&lt;/code&gt; as its error value for an overlong key, and that matches the &lt;code&gt;*&lt;/code&gt; stored for an account with no password. The &lt;a href="https://github.com/openssh/openssh-portable/commit/4353fdc43ef646c43a8dda7025cc1a898caf1482" rel="noopener noreferrer"&gt;fix commit&lt;/a&gt; refuses a match when the stored field is shorter than &lt;code&gt;MIN_PASSWORD_HASH_LEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cveawg.mitre.org/api/cve/CVE-2026-123158" rel="noopener noreferrer"&gt;CVE record&lt;/a&gt; scores it CVSS 3.1 8.1. CISA's assessment when I checked on 2026-11-06 was public PoC, not automatable. The record names Alpine as an example, while the Alpine advisory itself did not cite the CVE number on that date. The notices and reports I read mention no exploitation in the wild. That proves nothing, so the log check further down is still worth doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  These setups are not affected
&lt;/h3&gt;

&lt;p&gt;This list follows the Alpine advisory and the OpenSSH notice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers that don't run sshd. I opened the default &lt;code&gt;alpine:3.22&lt;/code&gt;, &lt;code&gt;3.23&lt;/code&gt; and &lt;code&gt;3.24&lt;/code&gt; images and none contained sshd.&lt;/li&gt;
&lt;li&gt;Servers using &lt;code&gt;openssh-server-pam&lt;/code&gt; with &lt;code&gt;UsePAM yes&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Servers with &lt;code&gt;PasswordAuthentication no&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Linux without musl. The notice targets Linux with musl libc and PAM disabled.&lt;/li&gt;
&lt;li&gt;Cloud images configured through cloud-init, which writes &lt;code&gt;PasswordAuthentication no&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;root&lt;/code&gt; is only directly reachable with &lt;code&gt;PermitRootLogin yes&lt;/code&gt;, even when its field is &lt;code&gt;*&lt;/code&gt;. The defaults I measured were &lt;code&gt;without-password&lt;/code&gt; on 3.22 and &lt;code&gt;prohibit-password&lt;/code&gt; on 3.23 and 3.24. The advisory adds that another account able to become root through &lt;code&gt;doas&lt;/code&gt; still gets there.&lt;/p&gt;

&lt;p&gt;Official tiny-cloud images from 3.18.11 through 3.24.3 ship an &lt;code&gt;alpine&lt;/code&gt; account with a &lt;code&gt;*&lt;/code&gt; field and passwordless &lt;code&gt;doas&lt;/code&gt;. They are safe on first boot, and the conditions are met as soon as a &lt;code&gt;$6$&lt;/code&gt; hash exists, for example from a console password.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same ssh -V, different patch state: the revision decides
&lt;/h2&gt;

&lt;p&gt;Two packages can print the same &lt;code&gt;OpenSSH_10.0p2&lt;/code&gt; and still differ in revision and binary, because Alpine's stable branches backported the fix instead of moving to 10.6p2. The vulnerable packages are already gone from the 3.22, 3.23 and 3.24 repository listings. So I took &lt;code&gt;10.0_p1-r10&lt;/code&gt; from the &lt;code&gt;alpine-virt-3.22.7&lt;/code&gt; ISO after verifying its signature, and installed it in a container with no network. I never started that sshd and only compared versions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Before the fix (Alpine 3.22.7 + package from the ISO)
$ ssh -V
OpenSSH_10.0p2, OpenSSL 3.5.9 29 Sep 2026
$ apk info -v | grep openssh-server-1
openssh-server-10.0_p1-r10
$ sha256sum /usr/sbin/sshd   # first 16 characters
2a78bc6d0cabd9bb  /usr/sbin/sshd

## After the fix (Alpine 3.22.8)
$ ssh -V
OpenSSH_10.0p2, OpenSSL 3.5.9 29 Sep 2026
$ apk info -v | grep openssh-server-1
openssh-server-10.0_p1-r11
$ sha256sum /usr/sbin/sshd   # first 16 characters
54fbd1f03a0aeeb4  /usr/sbin/sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyv3v9ug1302u7chq4isj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyv3v9ug1302u7chq4isj.webp" alt="ssh -V prints the same line for the vulnerable r10 and the fixed r11, while the package revision and sshd binary hash differ" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two &lt;code&gt;ssh -V&lt;/code&gt; lines are identical to the character. Only &lt;code&gt;r10&lt;/code&gt; versus &lt;code&gt;r11&lt;/code&gt; in &lt;code&gt;apk info -v&lt;/code&gt; and the sha256 differ. Note also that the package is named &lt;code&gt;10.0_p1&lt;/code&gt; while &lt;code&gt;ssh -V&lt;/code&gt; prints &lt;code&gt;10.0p2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The CVE record lists versions below 10.6p2 as affected, so a plain version string comparison flags the patched &lt;code&gt;r11&lt;/code&gt; as vulnerable. Reading &lt;code&gt;10.0p2&lt;/code&gt; as "patched" is just as wrong.&lt;/p&gt;

&lt;p&gt;These are the fixed versions from the advisory. At or above this version on a branch, the fix is in:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Fixed &lt;code&gt;openssh&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Measured &lt;code&gt;ssh -V&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;edge&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.6_p2-r0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.24&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.3_p1-r2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OpenSSH_10.3p1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.23&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.2_p1-r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OpenSSH_10.2p1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.22&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0_p1-r11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OpenSSH_10.0p2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.21&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.9_p2-r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.20&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.7_p1-r6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.19&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.6_p1-r3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.18&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.3_p2-r4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.17&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.1_p1-r7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.16&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.0_p1-r6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not measured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  No usepam line in sshd -T means a build without PAM
&lt;/h2&gt;

&lt;p&gt;A missing &lt;code&gt;usepam&lt;/code&gt; line is not a safe result. The default &lt;code&gt;openssh-server&lt;/code&gt; package is built without PAM, and that build's &lt;code&gt;sshd -T&lt;/code&gt; prints no &lt;code&gt;usepam&lt;/code&gt; line at all. &lt;strong&gt;The missing line is the build that meets the condition.&lt;/strong&gt; Skipping past an empty &lt;code&gt;grep usepam&lt;/code&gt; flips your verdict:&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="nv"&gt;$ &lt;/span&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(usepam|passwordauthentication) '&lt;/span&gt;
passwordauthentication &lt;span class="nb"&gt;yes&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;sshd.pam &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^(usepam|passwordauthentication) '&lt;/span&gt;   &lt;span class="c"&gt;# openssh-server-pam&lt;/span&gt;
usepam no
passwordauthentication &lt;span class="nb"&gt;yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing &lt;code&gt;openssh-server-pam&lt;/code&gt; is not enough either. Its default is &lt;code&gt;usepam no&lt;/code&gt;, which the advisory lists as affected. That package also ships &lt;code&gt;sshd.pam&lt;/code&gt; and no &lt;code&gt;sshd&lt;/code&gt; binary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Install state&lt;/th&gt;
&lt;th&gt;Binary&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;usepam&lt;/code&gt; in &lt;code&gt;sshd -T&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openssh-server&lt;/code&gt; default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sshd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no line&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openssh-server&lt;/code&gt; + &lt;code&gt;UsePAM yes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sshd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no line&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openssh-server-pam&lt;/code&gt; default&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sshd.pam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;no&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;met&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;openssh-server-pam&lt;/code&gt; + &lt;code&gt;UsePAM yes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sshd.pam&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not met&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second row is the trap. Adding &lt;code&gt;UsePAM yes&lt;/code&gt; to the default package only printed an &lt;code&gt;Unsupported option UsePAM&lt;/code&gt; warning. &lt;code&gt;sshd -t&lt;/code&gt; still exited 0 and the &lt;code&gt;usepam&lt;/code&gt; line stayed absent, so passing the config test does not mean PAM is on.&lt;/p&gt;

&lt;h2&gt;
  
  
  One read-only script gives the verdict
&lt;/h2&gt;

&lt;p&gt;The script below prints all of the above in one pass. It writes no files, opens no network connection and never attempts a login. From &lt;code&gt;/etc/shadow&lt;/code&gt; it prints only &lt;code&gt;*&lt;/code&gt; account names and counts, never a hash. Run it as root on the server itself:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# musl-sshd-check.sh — read-only exposure check for CVE-2026-123158 (OpenSSH + musl, no PAM).&lt;/span&gt;
&lt;span class="c"&gt;# Reads local package data, sshd -T output, /etc/shadow and /var/log/messages.&lt;/span&gt;
&lt;span class="c"&gt;# Writes nothing, opens no network connection, never attempts a login and never prints a hash.&lt;/span&gt;
&lt;span class="c"&gt;# Run as root on the machine itself: sh musl-sshd-check.sh&lt;/span&gt;

&lt;span class="nv"&gt;LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/var/log/messages

&lt;span class="c"&gt;# Fixed revisions from the Alpine advisory of 2026-11-05.&lt;/span&gt;
fixed_for&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;edge&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;10.6_p2-r0 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.24&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;10.3_p1-r2 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.23&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;10.2_p1-r1 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.22&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;10.0_p1-r11 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.21&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.9_p2-r1 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.20&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.7_p1-r6 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.19&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.6_p1-r3 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.18&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.3_p2-r4 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.17&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.1_p1-r7 &lt;span class="p"&gt;;;&lt;/span&gt;
    3.16&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;9.0_p1-r6 &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;SSHD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;c &lt;span class="k"&gt;in &lt;/span&gt;sshd sshd.pam sshd.krb5&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;SSHD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done

if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSHD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sshd_binary: none"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: NOT AFFECTED (no OpenSSH sshd installed)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sshd_binary: &lt;/span&gt;&lt;span class="nv"&gt;$SSHD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 1. libc&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;ldd &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSHD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'musl'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;libc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;musl&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else &lt;/span&gt;&lt;span class="nv"&gt;libc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;other&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"libc: &lt;/span&gt;&lt;span class="nv"&gt;$libc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$libc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; musl &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: NOT AFFECTED (sshd is not linked against musl)"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# 2. package revision&lt;/span&gt;
&lt;span class="nv"&gt;patched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /etc/alpine-release &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; apk &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;,2 /etc/alpine-release&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/alpine-release&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;_alpha&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;rel&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;edge &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="nv"&gt;pkg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;apk info &lt;span class="nt"&gt;-v&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^openssh-server(-pam|-krb5)?-[0-9]'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;ver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'s/^openssh-server(-pam|-krb5)?-//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;fixed_for &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rel&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"alpine_branch: &lt;/span&gt;&lt;span class="nv"&gt;$rel&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"package: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;pkg&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;not&lt;/span&gt;&lt;span class="p"&gt; found&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"fixed_revision: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;fix&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt; for this branch&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fix&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;apk version &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ver&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fix&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="s1"&gt;'&amp;lt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;patched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="p"&gt;;;&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;patched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;fi
else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"package: not an apk system, compare your vendor's advisory"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"patched: &lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 3. effective config. A build without PAM prints no usepam line at all.&lt;/span&gt;
&lt;span class="nv"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SSHD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-T&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sshd_T: failed (run as root, host keys required)"&lt;/span&gt;
  &lt;span class="nv"&gt;pwauth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
  &lt;span class="nv"&gt;usepam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nv"&gt;pwauth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$1=="passwordauthentication"{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;usepam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$1=="usepam"{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$usepam&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;usepam&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"absent(no PAM in this build)"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"permitrootlogin: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cfg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$1=="permitrootlogin"{print $2}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"passwordauthentication: &lt;/span&gt;&lt;span class="nv"&gt;$pwauth&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"usepam: &lt;/span&gt;&lt;span class="nv"&gt;$usepam&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="c"&gt;# sshd -T shows global values only, so a Match block can turn passwords back on.&lt;/span&gt;
&lt;span class="nv"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;inc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; /etc/ssh/sshd_config /etc/ssh/sshd_config.d/&lt;span class="k"&gt;*&lt;/span&gt;.conf&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-ciE&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*Match[[:space:]]'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;m+n&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;n&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;'{l=tolower($0)} l ~ /^[ \t]*match[ \t]/{i=1} i &amp;amp;&amp;amp; l ~ /^[ \t]*passwordauthentication[ \t=]+"?yes/{c++} END{print c+0}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;pm+n&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'^[[:space:]]*Include[[:space:]]'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vc&lt;/span&gt; &lt;span class="s1"&gt;'sshd_config\.d/\*\.conf'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;inc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;inc+n&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"match_blocks: &lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"match_password_yes: &lt;/span&gt;&lt;span class="nv"&gt;$pm&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"other_includes: &lt;/span&gt;&lt;span class="nv"&gt;$inc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 4. account state. Names of '*' accounts and counts only.&lt;/span&gt;
&lt;span class="nv"&gt;star_names&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nv"&gt;star&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;bang&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;sha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /etc/shadow &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;star_names&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="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'$2=="*"{printf "%s ", $1}'&lt;/span&gt; /etc/shadow&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;star&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="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'$2=="*"{n++} END{print n+0}'&lt;/span&gt; /etc/shadow&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;bang&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="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'$2 ~ /^!/{n++} END{print n+0}'&lt;/span&gt; /etc/shadow&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;sha&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="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'$2 ~ /^\$[56]\$/{n++} END{print n+0}'&lt;/span&gt; /etc/shadow&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"shadow_star_accounts: &lt;/span&gt;&lt;span class="nv"&gt;$star&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="nv"&gt;$star_names&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"shadow_bang_accounts: &lt;/span&gt;&lt;span class="nv"&gt;$bang&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"shadow_sha_hash_accounts: &lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"shadow: not readable (run as root)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Directory users through musl-nscd have no shadow entry, so the counts above miss them.&lt;/span&gt;
&lt;span class="nv"&gt;nscd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no
&lt;span class="k"&gt;if &lt;/span&gt;apk info &lt;span class="nt"&gt;-e&lt;/span&gt; musl-nscd &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-S&lt;/span&gt; /var/run/nscd/socket &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;nscd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"musl_nscd: &lt;/span&gt;&lt;span class="nv"&gt;$nscd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 5. listeners still using a replaced binary. Connections opened before the update&lt;/span&gt;
&lt;span class="c"&gt;# keep the old binary until they close, and restarting sshd does not end them.&lt;/span&gt;
&lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;d &lt;span class="k"&gt;in&lt;/span&gt; /proc/[0-9]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;/exe"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
  case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;e&lt;/span&gt;&lt;span class="p"&gt;% (deleted)&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;/sshd|&lt;span class="k"&gt;*&lt;/span&gt;/sshd.pam|&lt;span class="k"&gt;*&lt;/span&gt;/sshd.krb5|&lt;span class="k"&gt;*&lt;/span&gt;/sshd-session&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/sshd-auth&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;running&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;running+1&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;" (deleted)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;/cmdline"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"[listener]"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;stale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;stale+1&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;old&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;old+1&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"sshd_processes: &lt;/span&gt;&lt;span class="nv"&gt;$running&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"listeners_on_replaced_binary: &lt;/span&gt;&lt;span class="nv"&gt;$stale&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"connections_on_replaced_binary: &lt;/span&gt;&lt;span class="nv"&gt;$old&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# 6. password logins for accounts that have no password&lt;/span&gt;
&lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  for &lt;/span&gt;a &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$star_names&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Accepted password for &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;continue
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$usepam&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trace_note: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; 'Accepted password for &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt; (UsePAM yes, check how this user authenticates)"&lt;/span&gt;
    &lt;span class="k"&gt;else
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trace: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; 'Accepted password for &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nv"&gt;hits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;hits+n&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$nscd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    for &lt;/span&gt;u &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&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;'Accepted password for [^ ]*'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $4}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
      &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;:"&lt;/span&gt; /etc/shadow 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue
      &lt;/span&gt;&lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"Accepted password for &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt; "&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trace_note: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; 'Accepted password for &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;' in &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt; (no shadow entry, check how this user authenticates)"&lt;/span&gt;
    &lt;span class="k"&gt;done
  fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trace_hits: &lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"trace_hits: &lt;/span&gt;&lt;span class="nv"&gt;$LOG&lt;/span&gt;&lt;span class="s2"&gt; not readable or absent, check your log destination"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Verdict&lt;/span&gt;
&lt;span class="nv"&gt;pw_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pwauth&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; no &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pm&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;pw_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes
&lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$inc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;pw_open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nv"&gt;conditions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pw_open&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; no &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$usepam&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /etc/shadow &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;conditions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
  &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$star&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pw_open&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;conditions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes
  &lt;/span&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sha&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$star&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$nscd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nv"&gt;conditions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unknown
  &lt;span class="k"&gt;fi
fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hits&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: INVESTIGATE (password login recorded for an account with no password)"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$conditions&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: EXPOSED (old revision, all conditions met)"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$conditions&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; unknown &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: UPDATE NEEDED (old revision, conditions could not be confirmed)"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; no &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: UPDATE NEEDED (old revision, conditions not all met now)"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$stale&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: RESTART REQUIRED (package fixed, running listener is the old binary)"&lt;/span&gt;
&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$patched&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: PATCHED"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"VERDICT: UNKNOWN (compare your vendor's advisory, conditions met: &lt;/span&gt;&lt;span class="nv"&gt;$conditions&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the result on a default fixed 3.22.8 install. &lt;code&gt;root&lt;/code&gt; is &lt;code&gt;*&lt;/code&gt;, but the revision is fixed, so it ends in &lt;code&gt;PATCHED&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sshd_binary: /usr/sbin/sshd
libc: musl
alpine_branch: 3.22
package: openssh-server-10.0_p1-r11
fixed_revision: 10.0_p1-r11
patched: yes
permitrootlogin: without-password
passwordauthentication: yes
usepam: absent(no PAM in this build)
match_blocks: 0
match_password_yes: 0
other_includes: 0
shadow_star_accounts: 1 (root )
shadow_bang_accounts: 16
shadow_sha_hash_accounts: 0
musl_nscd: no
sshd_processes: 0
listeners_on_replaced_binary: 0
connections_on_replaced_binary: 0
trace_hits: /var/log/messages not readable or absent, check your log destination
VERDICT: PATCHED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fu5x2ckwwam35xfchd9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1fu5x2ckwwam35xfchd9.webp" alt="musl-sshd-check.sh output on a patched Alpine 3.22.8 container" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are seven possible verdicts. All test environments in the table were containers with no network.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;VERDICT&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;State I tested&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXPOSED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;old revision, conditions met&lt;/td&gt;
&lt;td&gt;r10 + one hash account, &lt;code&gt;sshd -T&lt;/code&gt; failing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UPDATE NEEDED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;old revision, conditions not met now or not confirmed&lt;/td&gt;
&lt;td&gt;r10 + no hash account, &lt;code&gt;sshd -T&lt;/code&gt; failing; the "not confirmed" paths are branch tests only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RESTART REQUIRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;package fixed, running sshd is the old file&lt;/td&gt;
&lt;td&gt;fixed sshd started, then the file replaced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCHED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;at or above the fixed revision&lt;/td&gt;
&lt;td&gt;fixed package defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INVESTIGATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;password login recorded for a &lt;code&gt;*&lt;/code&gt; account&lt;/td&gt;
&lt;td&gt;one synthetic log line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT AFFECTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no sshd, or not musl&lt;/td&gt;
&lt;td&gt;default image without sshd&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UNKNOWN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fixed version not known&lt;/td&gt;
&lt;td&gt;release changed to a branch outside the table&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two r10 rows are branch tests only. To avoid running the r10 sshd I masked it with another file, so &lt;code&gt;passwordauthentication&lt;/code&gt; printed &lt;code&gt;unknown&lt;/code&gt;, which the script treats as on. On a real server, run it as root so the value is read properly. I did not exercise the path for an sshd that isn't linked against musl.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPOSED&lt;/code&gt; is deliberately broad. &lt;code&gt;permitrootlogin&lt;/code&gt; is printed but not used in the verdict, so a server whose only &lt;code&gt;*&lt;/code&gt; account is &lt;code&gt;root&lt;/code&gt; with direct login blocked can still show &lt;code&gt;EXPOSED&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Even with a global &lt;code&gt;no&lt;/code&gt;, a &lt;code&gt;PasswordAuthentication yes&lt;/code&gt; inside a &lt;code&gt;Match&lt;/code&gt; block counts as open, and &lt;code&gt;match_password_yes&lt;/code&gt; is the number of those lines. I tested this by reading the fixed sshd's real config while faking only the &lt;code&gt;apk&lt;/code&gt; response as r10, and it printed &lt;code&gt;EXPOSED&lt;/code&gt;. The script reads only &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and &lt;code&gt;/etc/ssh/sshd_config.d/*.conf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It does not follow conditional &lt;code&gt;Include&lt;/code&gt; lines. So with a global &lt;code&gt;no&lt;/code&gt; plus a &lt;code&gt;Match&lt;/code&gt; block or an &lt;code&gt;Include&lt;/code&gt; of another path, it won't claim the conditions are unmet. It ends in &lt;code&gt;conditions could not be confirmed&lt;/code&gt;, and you check per-account values with &lt;code&gt;sshd -T -C&lt;/code&gt; as shown below.&lt;/p&gt;

&lt;p&gt;The script cannot count directory users. If &lt;code&gt;musl_nscd: yes&lt;/code&gt; and there is no local &lt;code&gt;*&lt;/code&gt; account, it also ends in &lt;code&gt;conditions could not be confirmed&lt;/code&gt;, and you need to check the directory accounts yourself. The same wording appears when it can't read &lt;code&gt;/etc/shadow&lt;/code&gt; because it isn't root.&lt;/p&gt;

&lt;p&gt;Check scripts and reproduction code are circulating online, and I ran none of them. Read any script before running it as root on a server, and that includes this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  After the package upgrade, restart sshd or you aren't done
&lt;/h2&gt;

&lt;p&gt;Replacing the package does not replace the running daemon. With a fixed sshd running, I swapped the file underneath it and the process kept holding the deleted old file. To avoid starting the vulnerable revision, this test reinstalled the same revision to replace only the file:&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="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/18/exe      &lt;span class="c"&gt;# before replacing&lt;/span&gt;
/usr/sbin/sshd
&lt;span class="nv"&gt;$ &lt;/span&gt;apk fix &lt;span class="nt"&gt;-r&lt;/span&gt; openssh-server
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/18/exe      &lt;span class="c"&gt;# package replaced, not restarted&lt;/span&gt;
/usr/sbin/sshd &lt;span class="o"&gt;(&lt;/span&gt;deleted&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/38/exe      &lt;span class="c"&gt;# after restart&lt;/span&gt;
/usr/sbin/sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order below follows the Alpine advisory:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep one SSH session open while you upgrade the package.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   apk upgrade &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Restart sshd.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   rc-service sshd restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Confirm key login works from a new session, then close the old one.&lt;/li&gt;
&lt;li&gt;Run the check script again and confirm &lt;code&gt;RESTART REQUIRED&lt;/code&gt; is gone.&lt;/li&gt;
&lt;li&gt;Rebuild any container image with sshd baked in. Per the advisory, images built from 3.21.10, 3.22.8, 3.23.8 and 3.24.4 onward contain the fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;RESTART REQUIRED&lt;/code&gt; looks at the listener only. Connections opened before the update, like the session you kept in step 1, may hold the old file, but they are counted separately as &lt;code&gt;connections_on_replaced_binary&lt;/code&gt; and don't affect the verdict. The &lt;a href="https://raw.githubusercontent.com/alpinelinux/aports/3.22-stable/main/openssh/sshd.initd" rel="noopener noreferrer"&gt;Alpine OpenRC script&lt;/a&gt; only ends connections at system shutdown, so a restart won't lower that number. Closing those sessions will.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you can't upgrade yet, block it with PasswordAuthentication no
&lt;/h3&gt;

&lt;p&gt;The advisory says either the update or this setting closes the hole. Confirm key login works first, or you will lock yourself out. &lt;strong&gt;A drop-in line alone leaves any &lt;code&gt;Match&lt;/code&gt; override in place.&lt;/strong&gt; The advisory's full command also rewrites lines inside &lt;code&gt;Match&lt;/code&gt; blocks in &lt;code&gt;sshd_config&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;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/ssh/sshd_config.d
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'PasswordAuthentication no'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/ssh/sshd_config.d/00-no-passwords.conf
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;.bak &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'1i PasswordAuthentication no'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'s/^\(\s*PasswordAuthentication[^y#]*\)yes/\1no/i'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /etc/ssh/sshd_config
sshd &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rc-service sshd restart
sshd &lt;span class="nt"&gt;-T&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; passwordauthentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alpine reads the drop-in directory from 3.19 on, and earlier releases ignore it. The &lt;code&gt;sed&lt;/code&gt; keeps the original as &lt;code&gt;sshd_config.bak&lt;/code&gt;. It only touches &lt;code&gt;sshd_config&lt;/code&gt;, so a &lt;code&gt;Match&lt;/code&gt; inside a drop-in file has to be fixed by hand.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sshd -T&lt;/code&gt; shows global values only, so the final &lt;code&gt;no&lt;/code&gt; alone isn't proof. I added a &lt;code&gt;Match User keyonly&lt;/code&gt; block in fixed 3.22 and 3.24 containers, and both gave the same result:&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;# drop-in only&lt;/span&gt;
sshd &lt;span class="nt"&gt;-T&lt;/span&gt;: passwordauthentication no
sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keyonly: passwordauthentication &lt;span class="nb"&gt;yes&lt;/span&gt;
&lt;span class="c"&gt;# after the advisory's sed&lt;/span&gt;
sshd &lt;span class="nt"&gt;-T&lt;/span&gt;: passwordauthentication no
sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;keyonly: passwordauthentication no
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a server that uses &lt;code&gt;Match&lt;/code&gt;, query the value for a specific account like this. With &lt;code&gt;Match Address&lt;/code&gt;, replace &lt;code&gt;addr&lt;/code&gt; with a real client address too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sshd &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;account&amp;gt;,host&lt;span class="o"&gt;=&lt;/span&gt;localhost,addr&lt;span class="o"&gt;=&lt;/span&gt;127.0.0.1 | &lt;span class="nb"&gt;grep&lt;/span&gt; ^passwordauthentication
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Check Accepted password for lines first to see if you were hit
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;Accepted password for&lt;/code&gt; line on an account with no password should be treated as suspected compromise, because that account should not be able to log in with a password. This is also the detection method the advisory gives:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Accepted password for"&lt;/span&gt; /var/log/messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the account names with &lt;code&gt;shadow_star_accounts&lt;/code&gt; from the script. The script does the same comparison and prints &lt;code&gt;INVESTIGATE&lt;/code&gt; on a match. I tested that path with one synthetic log line only and have never seen a real compromise log.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;trace_note&lt;/code&gt; is a reference line. On &lt;code&gt;UsePAM yes&lt;/code&gt; servers, or for directory users with no shadow entry, PAM may have handled the login normally, so those records don't become &lt;code&gt;INVESTIGATE&lt;/code&gt;. Check how that user actually authenticates.&lt;/p&gt;

&lt;p&gt;If you find such a record, or no logs survive, continue with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;doas&lt;/code&gt; usage records&lt;/li&gt;
&lt;li&gt;recently created users and files&lt;/li&gt;
&lt;li&gt;changes to &lt;code&gt;authorized_keys&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your server logs somewhere other than &lt;code&gt;/var/log/messages&lt;/code&gt;, look there, since the script reads only that file. The advisory says to treat the machine as compromised if you can't rule out a login.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;On Alpine, judge CVE-2026-123158 by the package revision rather than &lt;code&gt;ssh -V&lt;/code&gt;, then confirm the listener restarted. If you use &lt;code&gt;PasswordAuthentication no&lt;/code&gt; as a stopgap, check &lt;code&gt;Match&lt;/code&gt; overrides with &lt;code&gt;sshd -T -C&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Actual bypass behavior.&lt;/strong&gt; I did not reproduce the bypass or attempt a long-password login. The description comes from the OpenSSH notice, the CVE record and the Alpine advisory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether the fix blocks it.&lt;/strong&gt; I did not test that &lt;code&gt;r11&lt;/code&gt; actually stops the bypass. The patched verdict relies only on the advisory's version table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exploitation in the wild.&lt;/strong&gt; As of 2026-11-06 the notices and reports I read mention none. That doesn't mean there was none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenWrt, Buildroot, Yocto.&lt;/strong&gt; These come up because they use musl, but I found no official notice, and I did not check OpenWrt's default SSH server setup. The script is built around Alpine packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud images.&lt;/strong&gt; Statements about tiny-cloud and cloud-init images are quoted from the advisory. I did not boot a real cloud image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenRC restart path.&lt;/strong&gt; OpenRC doesn't run in the containers, so I did not run &lt;code&gt;rc-service sshd restart&lt;/code&gt;. I stopped and started the process directly and saw &lt;code&gt;(deleted)&lt;/code&gt; disappear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The vulnerable revision's sshd.&lt;/strong&gt; &lt;code&gt;r10&lt;/code&gt; was installed but its sshd was never run, so I have no &lt;code&gt;sshd -T&lt;/code&gt; output from it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vulnerable revisions outside 3.22.&lt;/strong&gt; My before/after comparison covers 3.22's &lt;code&gt;r10&lt;/code&gt; only. I did not confirm the same &lt;code&gt;ssh -V&lt;/code&gt; pattern on other branches with vulnerable packages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop-ins on 3.18 and older.&lt;/strong&gt; I confirmed drop-ins on 3.22 and 3.24 only. The 3.19 boundary is quoted from the advisory, and older releases need &lt;code&gt;sshd_config&lt;/code&gt; edited directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing connection handling.&lt;/strong&gt; Without logging in I could not test a real connection. I saw the branch only with a fake process, a busybox copy started under the name &lt;code&gt;sshd-session&lt;/code&gt;. The &lt;code&gt;[listener]&lt;/code&gt; title that identifies the listener was confirmed on 3.22, 3.23 and 3.24 only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;musl-nscd branch.&lt;/strong&gt; I tested this branch by faking the &lt;code&gt;apk&lt;/code&gt; response without installing musl-nscd, and never saw a real directory user setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public check scripts.&lt;/strong&gt; I did not run any external check script or reproduction code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NVD assessment.&lt;/strong&gt; I could not confirm the content of the NVD page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-11-06 first English version, based on Korean version 1.0 (container measurements on Alpine 3.22.8, 3.23.8, 3.24.4, version comparison with the vulnerable &lt;code&gt;r10&lt;/code&gt;, read-only check script)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>linux</category>
      <category>ssh</category>
      <category>devops</category>
    </item>
    <item>
      <title>NIKKE PC launcher hack: was my PC infected? k2f_chan in debug.log, 4 traces to check, accounts to rotate (2026-11-04)</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Thu, 05 Nov 2026 00:35:45 +0000</pubDate>
      <link>https://dev.to/ilan_kim/nikke-pc-launcher-hack-was-my-pc-infected-k2fchan-in-debuglog-4-traces-to-check-accounts-to-21bn</link>
      <guid>https://dev.to/ilan_kim/nikke-pc-launcher-hack-was-my-pc-infected-k2fchan-in-debuglog-4-traces-to-check-accounts-to-21bn</guid>
      <description>&lt;p&gt;If the NIKKE PC launcher's &lt;code&gt;debug.log&lt;/code&gt; contains &lt;code&gt;k2f_chan&lt;/code&gt;, the malicious agent ran on that PC, and the official notice of 2026-11-05 also treats that line as a sign it may have run. Deleting the files is not enough: change every password, session and API key that was on that PC, and do it from a different device.&lt;/p&gt;

&lt;p&gt;This is for anyone who has started the NIKKE: Goddess of Victory PC launcher recently and wants to judge their own machine. My Windows PC was actually infected on the evening of 2026-11-03, and it held Claude Code and Codex settings and development API keys. I checked it read-only on 2026-11-04, and the PowerShell script I wrote to look for the same traces is below. Two official notices came out the next day, so this version includes them too. All times are KST (UTC+9).&lt;/p&gt;

&lt;h2&gt;
  
  
  k2f_chan in NIKKE's debug.log means your PC was infected
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;debug.log&lt;/code&gt; had a burst of infection markers at 19:25 on November 3. Here is what my read-only check script printed. For the lines that contain the computer name or a UUID, I kept only the line number and time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== &amp;lt;drive&amp;gt;:\NIKKE\launcher\intl_service\debug.log
size=21872758  LastWriteTime=2026-11-03 20:26:05 (local)
total_lines=120580
-- 'k2f_chan' hits=1
   line 120569: [1103/192510.949:INFO:CONSOLE(1)] "k2f_chan registered", source:  (1)
-- 'lua downloaded to' hits=1
   line 120567  ts=1103/192510.909
-- 'adopted' hits=1
   line 120570  ts=1103/192511.005
-- 'agent started as' hits=1
   line 120571  ts=1103/192511.007
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fln9mw9f2tg4rmmxokjow.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fln9mw9f2tg4rmmxokjow.webp" alt="The five infection markers found in debug.log, redrawn by line number and time; the adopted and agent started as lines are replaced with anonymized placeholders" width="799" height="276"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image is not the raw log. It redraws the check result in line order. Only the &lt;code&gt;k2f_chan registered&lt;/code&gt; line is verbatim; the others are the marker strings listed in the &lt;a href="https://gist.github.com/MotherSphere/d6c049a09ba8ac3085c51ee9df24a4e5" rel="noopener noreferrer"&gt;public analysis gist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The prefix &lt;code&gt;[1103/192510.949]&lt;/code&gt; means November 3, 19:25:10, in &lt;strong&gt;the PC's local time&lt;/strong&gt;. The last line of the file, &lt;code&gt;1103/202605&lt;/code&gt;, matched the file's last-write time of 20:26:05 KST. The gist takes even a single &lt;code&gt;k2f_chan&lt;/code&gt; line to mean the agent ran. By the official manual-check notice, a &lt;code&gt;k2f_chan registered&lt;/code&gt; line means it may have run.&lt;/p&gt;

&lt;p&gt;The log is at &lt;code&gt;Launcher\intl_service\debug.log&lt;/code&gt; inside your NIKKE install folder. You can open it in Notepad and search, as the notice suggests, but mine was 21.9 MB. In PowerShell, one line does it; change the path to your own install folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\NIKKE\Launcher\intl_service\debug.log"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"k2f_chan"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-SimpleMatch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If even one line comes back, the PC was infected. If nothing comes back, this log shows no sign the agent ran. The notice still says that doesn't fully rule out risk and recommends a full antivirus scan. If you deleted or reinstalled the NIKKE folder, this log is gone with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the launcher in that window doesn't mean you were infected
&lt;/h3&gt;

&lt;p&gt;Since September 27, my log has launcher records on 18 days. &lt;code&gt;k2f_chan&lt;/code&gt; appears on only one of them, November 3. Even after the official start of distribution, there were 4 days with launcher records and no load.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Period&lt;/th&gt;
&lt;th&gt;Days with launcher records&lt;/th&gt;
&lt;th&gt;&lt;code&gt;k2f_chan&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Official timeline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;9/28 to 10/29&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;after tampering, before distribution started&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10/30 to 11/2&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;after distribution started (10/30 03:52)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11/3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1 (19:25:10)&lt;/td&gt;
&lt;td&gt;after distribution started&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero hits up to October 29 is expected, because by the notice that was before distribution began. Why the four days from the evening of October 30 through November 2 are empty, the log alone can't tell. Judge by your own log, not by the dates.&lt;/p&gt;

&lt;p&gt;The second launcher on the same PC, &lt;code&gt;NIKKE_GOOGLE&lt;/code&gt;, has no records for this period; its log ends on June 13.&lt;/p&gt;

&lt;p&gt;The official notice targets anyone who started the Windows PC launcher at least once after 15:41 on September 27. &lt;strong&gt;The notice says the mobile version is not affected.&lt;/strong&gt; If you only play on mobile, you don't need to check.&lt;/p&gt;

&lt;p&gt;The gist says Linux via Proton and the Steam Deck are affected too, except that the Chrome-side traces don't appear under Proton. Neither the notice nor the gist covers the Microsoft Store version or macOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  The NIKKE PC launcher hack started with tampering on September 27
&lt;/h2&gt;

&lt;p&gt;According to the official notice of 2026-11-05, the incident began on September 27. That morning a third party took over &lt;code&gt;insightjs.com&lt;/code&gt;, a domain used by some game services. That afternoon, four scripts on the official CDN that the launcher downloads were tampered with.&lt;/p&gt;

&lt;p&gt;Distribution of the malware to players' PCs started at 03:52 on October 30, through the tampered scripts. The table below puts the gist's observations into the notice's timeline. I converted the gist's GMT times to KST. Only the 11-03 19:25 row is my own measurement.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time (KST)&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;09-27 10:59&lt;/td&gt;
&lt;td&gt;Third party takes over &lt;code&gt;insightjs.com&lt;/code&gt;, a domain used by some game services&lt;/td&gt;
&lt;td&gt;notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;09-27 15:41 to 15:42&lt;/td&gt;
&lt;td&gt;Four official CDN scripts the launcher downloads are tampered with (&lt;code&gt;aegis-fix.min.js&lt;/code&gt;, &lt;code&gt;images.7fec4c32.js&lt;/code&gt;, &lt;code&gt;FeedList.feb647c9.js&lt;/code&gt;, &lt;code&gt;vue-bucket.min.js&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;notice; file names from the gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-24 01:40&lt;/td&gt;
&lt;td&gt;First report of the agent running&lt;/td&gt;
&lt;td&gt;gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-29 00:46&lt;/td&gt;
&lt;td&gt;Only the origin server of &lt;code&gt;FeedList.feb647c9.js&lt;/code&gt; gets the clean file; some CDNs keep serving the tampered one&lt;/td&gt;
&lt;td&gt;gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-30 03:52&lt;/td&gt;
&lt;td&gt;Malware distribution to players' PCs starts through the tampered scripts&lt;/td&gt;
&lt;td&gt;notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-03 19:25&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;k2f_chan registered&lt;/code&gt; on my PC&lt;/td&gt;
&lt;td&gt;my measurement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 around 00:00&lt;/td&gt;
&lt;td&gt;Anomaly detected, investigation starts&lt;/td&gt;
&lt;td&gt;notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 01:11&lt;/td&gt;
&lt;td&gt;First public warning on a NIKKE community Discord&lt;/td&gt;
&lt;td&gt;gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 02:04 to 02:23&lt;/td&gt;
&lt;td&gt;Three abnormal files replaced (first pass)&lt;/td&gt;
&lt;td&gt;notice (the gist says until 02:21)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 03:31&lt;/td&gt;
&lt;td&gt;Attacker server stops responding&lt;/td&gt;
&lt;td&gt;gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 07:26&lt;/td&gt;
&lt;td&gt;Last sighting of two CDN nodes serving the tampered &lt;code&gt;FeedList&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;gist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-04 12:09&lt;/td&gt;
&lt;td&gt;CDN cache refresh for the remaining file completed, then rolled out to nodes one by one (second pass)&lt;/td&gt;
&lt;td&gt;notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11-05 08:00&lt;/td&gt;
&lt;td&gt;Official PC client starts automatic malware scan&lt;/td&gt;
&lt;td&gt;notice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The gist's first report of the agent running (10-24 01:40) is six days earlier than the notice's start of distribution (10-30 03:52).&lt;/strong&gt; Neither source explains the gap, so the table keeps both times side by side with their sources.&lt;/p&gt;

&lt;p&gt;The two notices went up on 2026-11-05 on the &lt;a href="https://nikke-kr.com/news.html" rel="noopener noreferrer"&gt;official Korean NIKKE site's notice board&lt;/a&gt; (Korean). One covers what happened and the follow-up measures; the other is a manual-check guide. I read them at 08:35 that day. I didn't check whether the global site carries English versions, so everything I attribute to "the notice" here is my summary of the Korean posts, not a quote.&lt;/p&gt;

&lt;p&gt;For prevention, the notice lists stronger monitoring, integrity checks on external resources, and least privilege. Before the notices, on November 4, both Shift Up and the publisher Level Infinite only told Korean press they were looking into it (&lt;a href="https://www.yna.co.kr/view/AKR20261104049300017" rel="noopener noreferrer"&gt;Yonhap&lt;/a&gt;, &lt;a href="https://www.techm.kr/news/articleView.html?idxno=156892" rel="noopener noreferrer"&gt;TechM&lt;/a&gt;, both Korean). &lt;a href="https://www.invenglobal.com/articles/28099/nikke-pc-launcher-reportedly-hit-by-supply-chain-attack-no-statement-yet-from-level-infinite" rel="noopener noreferrer"&gt;Inven Global&lt;/a&gt; covered it in English on November 3, before any statement.&lt;/p&gt;

&lt;p&gt;Reports differ slightly on the number of tampered files and the start date, so this post follows only the notices and the gist. The gist also lists the attacker's server addresses and an IP blocklist, but I left them out of this post.&lt;/p&gt;

&lt;p&gt;What the gist says the agent collects is broad. It includes passwords and cookies saved in browsers and the NIKKE login saved in the launcher. On the developer side it looks at SSH keys, API tokens, &lt;code&gt;.env&lt;/code&gt; files and cloud credentials. On top of that come 28 wallet extensions, screenshots of every monitor, and remote command execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A read-only script checks all four traces at once
&lt;/h2&gt;

&lt;p&gt;Besides &lt;code&gt;debug.log&lt;/code&gt;, the official notice names four traces to delete. The script below reads those four plus the log, and that's all. It deletes nothing, changes nothing, and connects to nothing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trace&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Launcher web cache&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%APPDATA%\nikke_launcher\tbs_cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Survives deleting the NIKKE folder; &lt;code&gt;k2f.agent&lt;/code&gt; in its leveldb&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome persistence DLL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;%LOCALAPPDATA%\Google\Chrome\User Data\PlatformExperienceHelper\&amp;lt;64 hex chars&amp;gt;\platform_experience_helper.dll&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;PlatformExperienceHelper&lt;/code&gt; folder itself is a normal part of Chrome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COM hijack key&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HKCU\Software\Classes\CLSID\{E77CC89B-7401-4C04-8CED-149DB35ADD04}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Don't delete the same ID under HKLM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Launcher download log&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Launcher\log\tinyget\tinyget.&amp;lt;date&amp;gt;-&amp;lt;time&amp;gt;.log&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only the file that is exactly 205,135 bytes (about 200 KB), per the notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temp files&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;%TEMP%\k2f-bcreds-*&lt;/code&gt;, &lt;code&gt;%TEMP%\k2f_screen_*.jpg&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Gist indicators outside the four; deleted right after use, so absence proves nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Paste the script into Notepad and save it as &lt;code&gt;nikke-check.ps1&lt;/code&gt;. Open PowerShell in that folder and run it like this. No admin rights are needed, and &lt;code&gt;-ExecutionPolicy Bypass&lt;/code&gt; applies only to this one run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;powershell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-NoProfile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Bypass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\nikke-check.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything the script can't read is printed as &lt;code&gt;[ERROR]&lt;/code&gt;, and the result line then says the check is incomplete. If it can't find your install folder, pass it with &lt;code&gt;-NikkeDir&lt;/code&gt;, pointing at the folder that contains &lt;code&gt;Launcher&lt;/code&gt;. The script is ASCII-only with English output, because PowerShell 5.1 reads a script without a BOM in the system code page. Here is the full script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# nikke-check.ps1 - NIKKE PC launcher compromise (2026-10~11) read-only check&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Reads files and registry only. Deletes nothing, changes nothing, connects to nothing.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Output is printed to this window only. Indicators follow the public analysis gist&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# "NIKKE PC launcher compromise (November 2026)" by MotherSphere and the official&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# NIKKE notices of 2026-11-05 (tinyget file size).&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Usage (normal PowerShell, no admin needed):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;#   powershell -NoProfile -ExecutionPolicy Bypass -File .\nikke-check.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;#   powershell -NoProfile -ExecutionPolicy Bypass -File .\nikke-check.ps1 -NikkeDir "D:\Games\NIKKE"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;param&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$NikkeDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="bp"&gt;$Error&lt;/span&gt;&lt;span class="n"&gt;ActionPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'SilentlyContinue'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$label&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]]&lt;/span&gt;&lt;span class="nv"&gt;$detail&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="s2"&gt;"[FOUND] &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$detail&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&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="s2"&gt;"        &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&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="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="o"&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$label&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="s2"&gt;"[OK]    &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$label&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="s2"&gt;"[NOTE]  &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&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="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$label&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="s2"&gt;"[ERROR] &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&lt;/span&gt;&lt;span class="o"&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="s2"&gt;"NIKKE PC launcher check (read-only)  "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'yyyy-MM-dd HH:mm'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 1. debug.log : k2f_chan = the malicious agent ran on this PC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$NikkeDir&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="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$NikkeDir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$NikkeDir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Launcher'&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="nv"&gt;$un&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s1"&gt;'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s1"&gt;'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$un&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisplayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'NIKKE'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UninstallString&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="n"&gt;Split-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UninstallString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'^"([^"]+)".*$'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'$1'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Parent&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-PSDrive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PSProvider&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FileSystem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Root&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="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'NIKKE\Launcher'&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="nv"&gt;$logs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ForEach-Object&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="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'intl_service\debug.log'&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&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="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Unique&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$logs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug.log not found. Re-run with -NikkeDir pointing to the folder that holds Launcher."&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$logs&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"debug.log: &lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt;  (last write "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yyyy-MM-dd HH:mm'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nx"&gt;try&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="nv"&gt;$h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'k2f_chan'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-SimpleMatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug.log: k2f_chan x&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="s2"&gt;"line &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LineNumber&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;0&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="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="nx"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug.log: no k2f_chan"&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'lua downloaded to'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'agent started as'&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="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-SimpleMatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug.log: '&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="s2"&gt;' x&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&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="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="kr"&gt;catch&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"debug.log could not be read: &lt;/span&gt;&lt;span class="nv"&gt;$log&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 2. Launcher\log\tinyget : tinyget.&amp;lt;date&amp;gt;-&amp;lt;time&amp;gt;.log of exactly 205,135 bytes (official notice, 2026-11-05)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;#    Name, size and time only - the file content is not read.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$tg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="nv"&gt;$dirs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ForEach-Object&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="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'log\tinyget'&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Where-Object&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="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Unique&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Launcher\log\tinyget not found (same folders as debug.log were checked)"&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tg&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="kr"&gt;try&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="nv"&gt;$m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;205135&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'^tinyget\.\d{8}-\d{6}.*\.log$'&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tinyget: 205,135-byte log x&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'  (last write '&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastWriteTime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yyyy-MM-dd HH:mm'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tinyget: no 205,135-byte tinyget.&amp;lt;date&amp;gt;-&amp;lt;time&amp;gt;.log in &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;catch&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tinyget folder could not be listed: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 3. launcher web cache (survives deleting the NIKKE folder)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$tbs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APPDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'nikke_launcher\tbs_cache'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tbs&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="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$unread&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ldb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tbs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Local Storage\leveldb'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ldb&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="kr"&gt;try&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="nv"&gt;$files&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$ldb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&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="kr"&gt;catch&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbs_cache leveldb could not be listed: &lt;/span&gt;&lt;span class="nv"&gt;$ldb&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$files&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="kr"&gt;try&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="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Select-String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="err"&gt;.FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'k2f.agent'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-SimpleMatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="kr"&gt;catch&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="nv"&gt;$unread&lt;/span&gt;&lt;span class="o"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$c&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbs_cache leveldb: k2f.agent x&lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$tbs&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="kr"&gt;else&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbs_cache exists, no k2f.agent string (compressed .ldb can hide it). Delete it before the next launch: &lt;/span&gt;&lt;span class="nv"&gt;$tbs&lt;/span&gt;&lt;span class="s2"&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$unread&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbs_cache leveldb: &lt;/span&gt;&lt;span class="nv"&gt;$unread&lt;/span&gt;&lt;span class="s2"&gt; file(s) could not be read (close the launcher and run again)"&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;span class="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tbs_cache: not present"&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="c"&gt;# 4. Chrome persistence: 64-hex folder holding platform_experience_helper.dll&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$peh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Google\Chrome\User Data\PlatformExperienceHelper'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;try&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="nv"&gt;$bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$peh&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="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$peh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'^[0-9a-fA-F]{64}$'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'platform_experience_helper.dll'&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="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chrome PlatformExperienceHelper: 64-hex folder with the DLL x&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$bad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&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;span class="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chrome PlatformExperienceHelper: no 64-hex folder with the DLL (the folder itself is normal)"&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;span class="kr"&gt;catch&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Chrome PlatformExperienceHelper could not be listed: &lt;/span&gt;&lt;span class="nv"&gt;$peh&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="c"&gt;# 5. COM hijack key (HKCU only - an HKLM key with the same id, if present, is normal)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{E77CC89B-7401-4C04-8CED-149DB35ADD04}'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\Classes\CLSID\&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&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;span class="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU CLSID &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt; exists"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"InprocServer32 = "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU:\Software\Classes\CLSID\&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt;\InprocServer32"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'(default)'&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="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU CLSID &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt; : not present"&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"HKLM CLSID &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&gt; : "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\SOFTWARE\Classes\CLSID\&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="s2"&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;span class="s1"&gt;'present'&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="kr"&gt;else&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="s1"&gt;'absent'&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="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;" (normal either way - do not delete)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 6. other HKCU COM entries loading a DLL from AppData&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;try&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="nv"&gt;$keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'HKCU:\Software\Classes\CLSID'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&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="kr"&gt;catch&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="nv"&gt;$keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU CLSID list could not be read (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keys&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="nv"&gt;$v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'InprocServer32'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s1"&gt;'(default)'&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'PlatformExperienceHelper'&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU CLSID &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; loads &lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&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="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'AppData'&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="n"&gt;Note&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKCU CLSID &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PSChildName&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; loads a DLL from AppData (check the vendor): &lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# 7. leftovers in %TEMP% (the agent deletes them right after use - absence proves nothing)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;try&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="nv"&gt;$t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-LiteralPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-cmatch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'^k2f[-_]'&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"%TEMP%: k2f-* / k2f_* x&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&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;span class="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"%TEMP%: no k2f-* / k2f_* items"&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;span class="kr"&gt;catch&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="n"&gt;Err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"%TEMP% could not be listed: &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;TEMP&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Message&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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="c"&gt;# 8. DLL loaded right now&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="err"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Modules&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ModuleName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-contains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'platform_experience_helper.dll'&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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="n"&gt;Hit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform_experience_helper.dll loaded in: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'('&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;')'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; -join ', ')"&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="kr"&gt;else&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="n"&gt;Ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform_experience_helper.dll: not loaded in any visible process"&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="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;found&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="s2"&gt;"RESULT: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; indicator(s) found. Follow the removal order, then change passwords from another device."&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="kr"&gt;elseif&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&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="s2"&gt;"RESULT: check incomplete - &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; item(s) could not be read. Close the launcher and run again."&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="kr"&gt;else&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="s2"&gt;"RESULT: no indicators found by this script. This is not proof of a clean PC."&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="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;found&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&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="s2"&gt;"        (&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;errors&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; item(s) could not be read - see [ERROR] lines)"&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;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fru7s4c0bb9i0bwwgk6tv.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fru7s4c0bb9i0bwwgk6tv.webp" alt="Output of nikke-check.ps1 on my PC on 2026-11-04, before the tinyget check was added: three FOUND lines from debug.log, every other trace OK" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot is from the 2026-11-04 run, so it has no tinyget line; I added that check in the 2026-11-05 revision.&lt;/p&gt;

&lt;p&gt;On my PC only &lt;code&gt;debug.log&lt;/code&gt; produced hits, three of them. &lt;strong&gt;The launcher cache, the Chrome DLL and the HKCU key were all absent.&lt;/strong&gt; I had already deleted &lt;code&gt;tbs_cache&lt;/code&gt; myself before the check. For the other two, all I can say is that they weren't there when I checked. This PC had no Chrome installed, but the gist says the DLL and key can appear even without Chrome.&lt;/p&gt;

&lt;p&gt;I listed the tinyget folder separately during the 11-04 check. The &lt;code&gt;lua downloaded to&lt;/code&gt; line at 19:25:10 on November 3 pointed to &lt;code&gt;tinyget.20261103-192508-898.log&lt;/code&gt;. No file with that name was left when I checked.&lt;/p&gt;

&lt;p&gt;The tinyget logs that were left were 69,902 bytes for November 1 and 2 and 90,382 bytes for November 3. There was no 205,135-byte file, the size the notice names, on my PC. I couldn't confirm whether the downloaded file was that file, or why it disappeared.&lt;/p&gt;

&lt;p&gt;The HKLM result differed from the gist. The gist says the same CLSID exists under HKLM as a normal key pointing to &lt;code&gt;wpc.dll&lt;/code&gt; on every Windows install. On my Windows 11 (build 26300) that key didn't exist at all. Either way, leave HKLM alone.&lt;/p&gt;

&lt;p&gt;A loose search for &lt;code&gt;k2f&lt;/code&gt; in &lt;code&gt;%TEMP%&lt;/code&gt; gives false positives. On my PC it matched five folders, all random names that happened to contain &lt;code&gt;K2F&lt;/code&gt;, like &lt;code&gt;&amp;lt;tool&amp;gt;-K2FRwQ&lt;/code&gt;. That's why the script only counts names starting with &lt;code&gt;k2f-&lt;/code&gt; or &lt;code&gt;k2f_&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also went through autostart entries separately. Run keys, the Startup folder and scheduled tasks had zero suspicious items, and so did HKCU COM entries loading a DLL from AppData. Defender's detection history was empty too, which means nothing here: Defender was off and a third-party antivirus was registered on that PC.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're infected, remove things in this order
&lt;/h2&gt;

&lt;p&gt;The easiest route is the official PC client's automatic scan. According to the notice, from 08:00 on 2026-11-05 the client finds and removes the malware itself. Close every browser, including Chrome, then start the client.&lt;/p&gt;

&lt;p&gt;If it finds something, a popup asks whether to delete the logs, the DLL and the registry entries, and agreeing deletes them. If nothing is found, the game just starts with no popup. The notice also says that if a browser wasn't fully closed and the DLL stays behind, it no longer runs.&lt;/p&gt;

&lt;p&gt;The manual-check notice calls this feature the ACE security feature. The gist had advised not to start the launcher until a fix was confirmed. Per the notice, replacement of the tampered files finished at 12:09 on November 4 and then rolled out to the CDN nodes one by one.&lt;/p&gt;

&lt;p&gt;To remove things by hand, follow the notice's order. First disconnect from the internet and fully quit NIKKE, its launcher and Chrome. Check the system tray icons too, and don't start the launcher again until you're done.&lt;/p&gt;

&lt;p&gt;The DLL is locked while it's loaded and can't be deleted, which is why the order matters:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In Registry Editor, delete &lt;code&gt;{E77CC89B-7401-4C04-8CED-149DB35ADD04}&lt;/code&gt; under &lt;code&gt;HKEY_CURRENT_USER\Software\Classes\CLSID&lt;/code&gt;. The same name exists under other paths too, so delete it only at this path.&lt;/li&gt;
&lt;li&gt;Restart Windows. The gist reports that the DLL stays locked even after closing every Chrome window.&lt;/li&gt;
&lt;li&gt;Inside &lt;code&gt;PlatformExperienceHelper&lt;/code&gt;, delete only the folder whose name is 64 hex characters and that contains &lt;code&gt;platform_experience_helper.dll&lt;/code&gt;. Leave the &lt;code&gt;PlatformExperienceHelper&lt;/code&gt; folder itself. If no such folder exists now, the notice reads that as the DLL not being installed.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;Launcher\log\tinyget&lt;/code&gt;, delete the file that is 205,135 bytes and named like &lt;code&gt;tinyget.&amp;lt;date&amp;gt;-&amp;lt;time&amp;gt;.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Delete the whole &lt;code&gt;%APPDATA%\nikke_launcher\tbs_cache&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;Update your antivirus and run a full scan, then also run a Microsoft Defender Offline scan from Windows Security.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A clean antivirus result doesn't prove removal. The gist says reinstalling Windows is the safest option.&lt;/p&gt;

&lt;p&gt;Under Proton, &lt;code&gt;tbs_cache&lt;/code&gt; lives inside the Proton prefix. The Chrome DLL and the registry key don't apply on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting the traces doesn't bring back leaked accounts and API keys
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Removing the traces doesn't undo what has already left the PC.&lt;/strong&gt; So do the account work on a different, clean device, not the infected PC. Here is the gist's recommended order as a table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Why&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;1&lt;/td&gt;
&lt;td&gt;NIKKE and Level Infinite account&lt;/td&gt;
&lt;td&gt;The login saved in the launcher is collected&lt;/td&gt;
&lt;td&gt;Change the password, turn on 2FA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Recovery path for your other accounts&lt;/td&gt;
&lt;td&gt;Change the password, turn on 2FA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Discord, Google, Steam, GitHub sessions&lt;/td&gt;
&lt;td&gt;A cookie alone logs in without the password&lt;/td&gt;
&lt;td&gt;Log out of all sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Banking, passwords saved in browsers&lt;/td&gt;
&lt;td&gt;Saved passwords from Chrome, Edge, Firefox and others are collected&lt;/td&gt;
&lt;td&gt;Change the passwords for saved sites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;API tokens, SSH keys, cloud credentials&lt;/td&gt;
&lt;td&gt;Developer credential files and &lt;code&gt;.env&lt;/code&gt; are collected&lt;/td&gt;
&lt;td&gt;Revoke the old keys and issue new ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Crypto wallets&lt;/td&gt;
&lt;td&gt;28 Chrome wallet extensions&lt;/td&gt;
&lt;td&gt;Move funds to a new wallet with a new seed phrase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Session logout is the step people skip. Changing a password can leave sessions opened with stolen cookies alive. Hit each service's "log out of all devices" option once.&lt;/p&gt;

&lt;p&gt;The notice's two account-protection steps overlap with this table. Changing the passwords of accounts used on that PC covers rows 1 to 4. Revoking and reissuing API keys and access tokens used or stored on that PC is row 5. The notice adds that you should do this from an unaffected device or from a PC that has finished the check.&lt;/p&gt;

&lt;p&gt;For damage to accounts outside the game, or financial loss, the notice says to contact official customer support.&lt;/p&gt;

&lt;p&gt;Here is what I actually did. I deleted &lt;code&gt;tbs_cache&lt;/code&gt; before the check and scanned with Norton 360, which found nothing. I changed passwords and logged out of every session. I revoked my Claude and OpenAI API keys and my GitHub token. Then I formatted Windows. That was the price of not keeping my gaming PC and my work PC separate.&lt;/p&gt;

&lt;p&gt;Community-made scanners are going around too. I didn't run any; I looked for the same traces with the read-only script in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;debug.log&lt;/code&gt; has a &lt;code&gt;k2f_chan registered&lt;/code&gt; line, treat the PC as infected: remove the traces in the notice's order or let the official client's scan do it, then rotate passwords, sessions and API keys from another device. Launching in that window doesn't mean infection by itself, so judge by your own log, and keep in mind that a clean scan doesn't prove removal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;The traces before removal: the only trace I deleted was &lt;code&gt;tbs_cache&lt;/code&gt;, and I didn't keep a screenshot. I couldn't confirm whether it held &lt;code&gt;k2f.agent&lt;/code&gt;, or whether the Chrome DLL and HKCU key existed before. My account of what I did relies on memory. The &lt;code&gt;tinyget.20261103-192508-898.log&lt;/code&gt; file that &lt;code&gt;debug.log&lt;/code&gt; pointed to was also gone at check time. I don't know whether it was the notice's 205,135-byte file, or when and why it disappeared.&lt;/p&gt;

&lt;p&gt;Whether anything actually leaked: no unusual account activity so far is not proof that nothing leaked. The PC-side logs can't show what the agent took. The gist also says remote commands leave nothing in the launcher logs.&lt;/p&gt;

&lt;p&gt;Why the agent didn't load on some days after distribution began: zero hits through October 29 is explained by the notice's start of distribution. From that start (03:52 on October 30) through November 2, there was still no &lt;code&gt;k2f_chan&lt;/code&gt; for four days. I don't know whether that was the CDN cache or some other condition.&lt;/p&gt;

&lt;p&gt;Microsoft Store, Google Play Games and macOS: the notice covers the Windows PC launcher and says mobile is unaffected. Neither the notice nor the gist covers the other versions. The notice's example paths include &lt;code&gt;NIKKE_GOOGLE&lt;/code&gt;, but that alone doesn't settle whether the Google Play Games version is affected. I only checked the PC launcher version.&lt;/p&gt;

&lt;p&gt;The script on Steam and Proton: the script finds the install folder from the uninstall registry entries and also tries &lt;code&gt;NIKKE\Launcher&lt;/code&gt; on every drive. I haven't run it against a Steam install path or under Proton.&lt;/p&gt;

&lt;p&gt;Whether the removal order works: the removal order here matches the notice's manual-check steps. But my PC had no HKCU key, no Chrome DLL and no 205,135-byte tinyget file at check time, so I couldn't walk through the steps myself.&lt;/p&gt;

&lt;p&gt;The official client's automatic scan: the scan has run since 08:00 on November 5, but I had already formatted that PC and couldn't try it. The detection conditions and popup behavior come only from the notice.&lt;/p&gt;

&lt;p&gt;The script's tinyget size check: the 205,135-byte check, added on 2026-11-05, is built only from the size and name pattern in the notice. It has never run on Windows, and I haven't confirmed that it fires on an infected PC.&lt;/p&gt;

&lt;p&gt;Other Level Infinite games: other games that use the same launcher components have been mentioned as possibly affected. This post checks only the NIKKE launcher.&lt;/p&gt;

&lt;p&gt;Community scanner results: I didn't run the community scanners, so I couldn't compare their results with this script's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-11-05 first English version, based on Korean version 1.1 (read-only check of my PC on 2026-11-04: one k2f_chan line in debug.log, no other traces; two official notices of 2026-11-05: timeline, tinyget trace, client auto-scan, mobile not affected)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>gaming</category>
      <category>windows</category>
      <category>powershell</category>
    </item>
    <item>
      <title>SillyTavern remote access security checklist: 11 CVEs in 2026, the Node Brotli fix (CVE-2026-111497), 6 config.yaml keys</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:52:26 +0000</pubDate>
      <link>https://dev.to/ilan_kim/sillytavern-remote-access-security-checklist-11-cves-in-2026-the-node-brotli-fix-11l8</link>
      <guid>https://dev.to/ilan_kim/sillytavern-remote-access-security-checklist-11-cves-in-2026-the-node-brotli-fix-11l8</guid>
      <description>&lt;p&gt;If you use SillyTavern from your phone or another PC, check three things now: the SillyTavern version, whether your Node.js has the 2026-10-21 security patch, and &lt;code&gt;privateAddressWhitelist&lt;/code&gt; in &lt;code&gt;config.yaml&lt;/code&gt;. The Node bug, CVE-2026-111497, went onto CISA's list of actively exploited vulnerabilities on 2026-10-27, and the official Docker image &lt;code&gt;latest&lt;/code&gt; still ships an unpatched Node as of 2026-10-28.&lt;/p&gt;

&lt;p&gt;This is a checklist for people who run SillyTavern, the self-hosted LLM chat frontend, with &lt;code&gt;listen: true&lt;/code&gt; and reach it from other devices. On 2026-10-28 I ran 1.17.0 and 1.19.0 on my Mac, and ran the same commands on six Node versions to find a check that tells patched from unpatched builds.&lt;/p&gt;

&lt;p&gt;I build backends in Node, and I was unpatched that morning too. My Mac was on v24.21.0, and all three Node versions on my Windows machine were pre-patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Without access control, SillyTavern refuses to start with listen: true
&lt;/h2&gt;

&lt;p&gt;On 1.19.0 I turned off both the whitelist and Basic auth and started the server with &lt;code&gt;listen: true&lt;/code&gt;. It printed one line and exited with code 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your current SillyTavern configuration is insecure (listening to non-localhost). Enable whitelisting, basic authentication or user accounts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if you already use it remotely, you probably have at least one access control switched on. The exception is &lt;code&gt;securityOverride: true&lt;/code&gt;. With that value the server only logs a warning and starts anyway.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa2bsfh8pmettn1x15s41.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa2bsfh8pmettn1x15s41.webp" alt="SillyTavern 1.19.0 startup log: without access control it prints the insecure line and exits; with the whitelist on it starts with the private address filter warning" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The line to watch is the warning you get once the whitelist is on.&lt;/strong&gt; It is a &lt;code&gt;Warning:&lt;/code&gt; line saying the private address filter is off. The filter arrived in 1.18.0 and is off by default, so the warning shows up on every start.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Blocked connection from&lt;/code&gt; line at the bottom of the capture is a connection the whitelist rejected. I connected to the same Mac through its LAN address and got a 403. That line comes back later, in the log check.&lt;/p&gt;

&lt;h2&gt;
  
  
  All 11 SillyTavern CVEs of 2026 are fixed in 1.18.0
&lt;/h2&gt;

&lt;p&gt;SillyTavern had 11 CVEs published in 2026. I counted them on 2026-10-28 by pulling every GitHub Security Advisory with &lt;code&gt;gh api&lt;/code&gt;. 1.16.0 fixed one, 1.17.0 fixed four, and 1.18.0 fixed six. Anything older than 1.18.0 is exposed to at least six of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CVE&lt;/th&gt;
&lt;th&gt;Fixed in&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;GHSA severity&lt;/th&gt;
&lt;th&gt;No auth needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-26286&lt;/td&gt;
&lt;td&gt;1.16.0&lt;/td&gt;
&lt;td&gt;SSRF in asset download&lt;/td&gt;
&lt;td&gt;high&lt;/td&gt;
&lt;td&gt;no vector listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-34522&lt;/td&gt;
&lt;td&gt;1.17.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/chats/import&lt;/code&gt; path traversal (file write)&lt;/td&gt;
&lt;td&gt;high 8.1&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-34523&lt;/td&gt;
&lt;td&gt;1.17.0&lt;/td&gt;
&lt;td&gt;Static file existence oracle&lt;/td&gt;
&lt;td&gt;medium 5.3&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-34524&lt;/td&gt;
&lt;td&gt;1.17.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/chats/export&lt;/code&gt; and &lt;code&gt;delete&lt;/code&gt; path traversal&lt;/td&gt;
&lt;td&gt;high 8.3&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-34526&lt;/td&gt;
&lt;td&gt;1.17.0&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/api/search/visit&lt;/code&gt; SSRF (incomplete validation)&lt;/td&gt;
&lt;td&gt;medium 5.0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-44648&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;Existing sessions survive a password change&lt;/td&gt;
&lt;td&gt;high 7.5&lt;/td&gt;
&lt;td&gt;yes (user interaction)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-44649&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;Authentication bypass via SSO headers&lt;/td&gt;
&lt;td&gt;critical 9.8&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-44650&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;Path traversal in extension deletion&lt;/td&gt;
&lt;td&gt;critical 9.1&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-44651&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;Reflected XSS in the CORS proxy&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;no vector listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-44652&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;SSRF in the CORS proxy&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;no vector listed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CVE-2026-46372&lt;/td&gt;
&lt;td&gt;1.18.0&lt;/td&gt;
&lt;td&gt;SSRF in the SearXNG search proxy&lt;/td&gt;
&lt;td&gt;high 8.5&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "No auth needed" column comes from whether the GHSA's CVSS vector says &lt;code&gt;PR:N&lt;/code&gt;. &lt;strong&gt;The most urgent one is CVE-2026-44650.&lt;/strong&gt; It is critical 9.1 and needs no login. CVE-2026-44649 scores 9.8, but it only applies if you turned on SSO header authentication.&lt;/p&gt;

&lt;p&gt;The two CORS proxy bugs only matter if you enabled &lt;code&gt;enableCorsProxy&lt;/code&gt;. Its default is &lt;code&gt;false&lt;/code&gt;. The GHSAs include reproduction steps; I'm not repeating them here.&lt;/p&gt;

&lt;p&gt;Four of this year's CVEs are SSRF. The maintainers' answer was to add &lt;code&gt;privateAddressWhitelist&lt;/code&gt; in 1.18.0, as written in &lt;a href="https://github.com/SillyTavern/SillyTavern/security/advisories/GHSA-qg89-qwwh-5f3j" rel="noopener noreferrer"&gt;GHSA-qg89-qwwh-5f3j&lt;/a&gt;. They also said future SSRF reports will only be accepted if they bypass that filter. In other words, turning the filter on is now your job.&lt;/p&gt;

&lt;p&gt;One more for background. CVE-2025-59159, from 2025-10, is a DNS rebinding bug rated critical 9.6. 1.13.4 added the &lt;code&gt;hostWhitelist&lt;/code&gt; setting to block it. That setting is also off by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node2Shell (CVE-2026-111497) reaches SillyTavern through the responses it fetches
&lt;/h2&gt;

&lt;p&gt;Node.js fixed CVE-2026-111497 in its 2026-10-21 security release and rated it High. The advisory title is "Brotli decoder out-of-bounds write permits remote code execution". If an application decompresses Brotli data from someone else with &lt;code&gt;node:zlib&lt;/code&gt;, process memory can get corrupted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details (checked 2026-10-28)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Patched versions&lt;/td&gt;
&lt;td&gt;v22.23.4, v24.22.1, v26.12.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No patch&lt;/td&gt;
&lt;td&gt;20.x (EOL 2026-04-30), 25.x (EOL 2026-06-01)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scores&lt;/td&gt;
&lt;td&gt;nodejs.org High, NVD CVSS 4.0 9.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root cause&lt;/td&gt;
&lt;td&gt;The Google Brotli decoder bundled in Node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KISA (Korea) Node.js advisory, 10-23&lt;/td&gt;
&lt;td&gt;Lists 3 other CVEs, not this one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploitation&lt;/td&gt;
&lt;td&gt;Added to CISA KEV 2026-10-27, remediation due 2026-11-10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Node2Shell is a community nickname; it appears on neither nodejs.org nor NVD. Searching by the CVE number gets you the right results. KISA is Korea's national internet security agency, and its Boho-nara site publishes Korean-language security advisories. If you only read &lt;a href="https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&amp;amp;nttId=72236&amp;amp;menuNo=205020" rel="noopener noreferrer"&gt;the KISA notice&lt;/a&gt;, you miss this one.&lt;/p&gt;

&lt;p&gt;Read &lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;the nodejs.org advisory&lt;/a&gt; itself. As of 2026-10-28, it does not mention any exploitation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CISA added this CVE to its KEV (Known Exploited Vulnerabilities) catalog on 2026-10-27.&lt;/strong&gt; NVD shows the same date and an SSVC &lt;code&gt;exploitation: active&lt;/code&gt; entry. The KEV entry names Google Brotli as the product. Neither source says the exploitation targeted SillyTavern or Node.js.&lt;/p&gt;

&lt;p&gt;SillyTavern sends requests to whatever API address you configure. I pointed it at a small server on my Mac that logs request headers, and this is what arrived:&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="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"/v1/models"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"*/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"accept-encoding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"gzip, deflate, br"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Bearer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"user-agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"node-fetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1:18080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"connection"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"close"&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;&lt;code&gt;accept-encoding&lt;/code&gt; includes &lt;code&gt;br&lt;/code&gt;. In 1.19.0, 30 files under &lt;code&gt;src&lt;/code&gt; use &lt;code&gt;node-fetch&lt;/code&gt;. &lt;code&gt;node-fetch&lt;/code&gt; 3.3.2 decodes &lt;code&gt;br&lt;/code&gt; responses with &lt;code&gt;zlib.createBrotliDecompress()&lt;/code&gt;. The global &lt;code&gt;fetch&lt;/code&gt; uses the same decoder.&lt;/p&gt;

&lt;p&gt;Incoming request bodies are a different story. &lt;code&gt;body-parser&lt;/code&gt; 1.20.4 only inflates &lt;code&gt;deflate&lt;/code&gt; and &lt;code&gt;gzip&lt;/code&gt;, and answers &lt;code&gt;br&lt;/code&gt; with a 415. &lt;strong&gt;So the path to this bug is the responses SillyTavern fetches.&lt;/strong&gt; API endpoints, the search proxy and asset URLs all count.&lt;/p&gt;

&lt;p&gt;I did not send a malicious Brotli response; I stopped at headers and source code. If you use an API proxy whose operator you don't know, I'd treat the Node patch as more urgent.&lt;/p&gt;

&lt;h2&gt;
  
  
  process.versions.brotli can't tell you whether you're patched
&lt;/h2&gt;

&lt;p&gt;Check the patch with &lt;code&gt;node -v&lt;/code&gt;. If that leaves you unsure, a single zlib constant settles it. I ran the same commands on six Node versions:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjnirbwa96gyppyw5jsy.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnjnirbwa96gyppyw5jsy.webp" alt="Version checks on six Node versions: process.versions.brotli is the same before and after the patch; only the BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH constant differs" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;process.versions.brotli&lt;/code&gt; reads &lt;code&gt;1.2.0&lt;/code&gt; on both v24.21.0 and v24.22.1. The 22 line shows &lt;code&gt;1.1.0&lt;/code&gt; before and after the patch. That's because Node did not upgrade the library; it cherry-picked the one fix.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH&lt;/code&gt; constant is what differs. The three patched builds have it and the three unpatched builds don't. So don't use &lt;code&gt;process.versions.brotli&lt;/code&gt; for this check.&lt;/p&gt;

&lt;p&gt;I ran the same check on my Windows machine. Node there is managed by &lt;code&gt;fnm&lt;/code&gt;, which I installed with winget. All three installed versions returned &lt;code&gt;false&lt;/code&gt; for the constant. Over a remote shell, &lt;code&gt;node&lt;/code&gt; wasn't found at all, because the PowerShell profile is what puts &lt;code&gt;fnm&lt;/code&gt; on the PATH.&lt;/p&gt;

&lt;h2&gt;
  
  
  The official Docker image latest ships Node 24.21.0 as of 2026-10-28
&lt;/h2&gt;

&lt;p&gt;I checked the registry metadata. The official &lt;code&gt;latest&lt;/code&gt; and &lt;code&gt;1.19.0&lt;/code&gt; images were both built on 2026-09-14. The &lt;code&gt;NODE_VERSION&lt;/code&gt; in both image configs is &lt;code&gt;24.21.0&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Built&lt;/th&gt;
&lt;th&gt;Bundled Node&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;latest&lt;/code&gt; (release)&lt;/td&gt;
&lt;td&gt;2026-09-14&lt;/td&gt;
&lt;td&gt;24.21.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.19.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-09-14&lt;/td&gt;
&lt;td&gt;24.21.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;staging&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2026-10-28&lt;/td&gt;
&lt;td&gt;24.22.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Hub &lt;code&gt;node:lts-alpine3.23&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2026-10-21&lt;/td&gt;
&lt;td&gt;24.22.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The official docs say "No action required" for Docker.&lt;/strong&gt; That only holds once the image is rebuilt. Pulling &lt;code&gt;latest&lt;/code&gt; again today gets you the same image.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Dockerfile&lt;/code&gt; starts from &lt;code&gt;node:lts-alpine3.23&lt;/code&gt;. That base image was rebuilt with 24.22.1 on 10-21. I expect a build from source to pick up the patched Node. &lt;code&gt;staging&lt;/code&gt; is the development branch, so feature changes come along with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check two versions, six settings and one log, in that order
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check the SillyTavern version
&lt;/h3&gt;

&lt;p&gt;Look for a line like &lt;code&gt;SillyTavern 1.19.0&lt;/code&gt; at the top of the startup log; it should say 1.18.0 or later. The server prints it before it accepts any request, so you see it whatever your auth settings are.&lt;/p&gt;

&lt;p&gt;On a local server without access control, &lt;code&gt;pkgVersion&lt;/code&gt; in the &lt;code&gt;/version&lt;/code&gt; response works too. With Basic auth on, this request gets a 401. Don't trust &lt;code&gt;isLatest&lt;/code&gt; in the same response, because it doesn't query the remote. The request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:8000/version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Check Node
&lt;/h3&gt;

&lt;p&gt;Run these in the same terminal you start SillyTavern from. The &lt;code&gt;Node version:&lt;/code&gt; line at the top of the startup log shows the same value:&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;-v&lt;/span&gt;
node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"'BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH' in require('zlib').constants"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;true&lt;/code&gt; means a patched build. For Docker, use &lt;code&gt;docker exec &amp;lt;container-name&amp;gt; node -v&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Update Node
&lt;/h3&gt;

&lt;p&gt;The route depends on how you installed it. The commands are from &lt;a href="https://docs.sillytavern.app/installation/updating/node/" rel="noopener noreferrer"&gt;the official Node update docs&lt;/a&gt;, except the &lt;code&gt;fnm&lt;/code&gt; row, which I added:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Install method&lt;/th&gt;
&lt;th&gt;Commands&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;nvm (macOS, Linux)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nvm install --lts&lt;/code&gt;, then &lt;code&gt;nvm use --lts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nvm-windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nvm install lts&lt;/code&gt;, then &lt;code&gt;nvm use lts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fnm&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fnm install 24.22.1&lt;/code&gt;, then &lt;code&gt;fnm use 24.22.1&lt;/code&gt; and &lt;code&gt;fnm default 24.22.1&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows winget&lt;/td&gt;
&lt;td&gt;&lt;code&gt;winget install --id=OpenJS.NodeJS.LTS -e&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homebrew&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;brew update&lt;/code&gt;, then &lt;code&gt;brew upgrade node&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Termux&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pkg update&lt;/code&gt;, then &lt;code&gt;pkg upgrade nodejs-lts&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Rebuild from source, or wait for a new image&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;After updating, run check 2 again and restart SillyTavern. With fnm, changing only &lt;code&gt;fnm default&lt;/code&gt; leaves the current shell on its old Node. In my run, the shell only switched to v24.22.1 after &lt;code&gt;fnm use&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Update SillyTavern to 1.19.0
&lt;/h3&gt;

&lt;p&gt;Since 1.17.0, &lt;code&gt;engines&lt;/code&gt; requires &lt;code&gt;&amp;gt;= 20&lt;/code&gt;, so update Node first.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Check six &lt;code&gt;config.yaml&lt;/code&gt; entries
&lt;/h3&gt;

&lt;p&gt;Defaults are from &lt;code&gt;default/config.yaml&lt;/code&gt; in 1.19.0.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;For remote access&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;whitelistMode&lt;/code&gt;, &lt;code&gt;whitelist&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, &lt;code&gt;::1&lt;/code&gt; and &lt;code&gt;127.0.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keep on, add only your phone's or VPN's address&lt;/td&gt;
&lt;td&gt;Anything outside the whitelist gets a 403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;basicAuthMode&lt;/code&gt;, &lt;code&gt;basicAuthUser&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;false&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;/&lt;code&gt;password&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Turn on and change the default password&lt;/td&gt;
&lt;td&gt;The default credentials are public&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;privateAddressWhitelist.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Covers the 4 SSRFs; the maintainers' recommendation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hostWhitelist.enabled&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;true&lt;/code&gt;, list the host names you use&lt;/td&gt;
&lt;td&gt;The setting that addresses CVE-2025-59159&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enableCorsProxy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CVE-2026-44651, CVE-2026-44652&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;securityOverride&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Keep &lt;code&gt;false&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Disables the startup check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The official docs recommend a VPN or a tunnel over port forwarding. If you're going to expose it straight to the internet anyway, get every row of this table right first.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. If an LLM server runs elsewhere on your LAN, add its range to &lt;code&gt;allowedRanges&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;By default the filter only allows loopback. A request to a backend on another PC gets blocked 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;[Private Request Filter] Blocked request to private IP address: &amp;lt;LAN-IP&amp;gt;
FetchError: request to http://&amp;lt;LAN-IP&amp;gt;:9/v1/models failed, reason: Blocked request to private IP address: &amp;lt;LAN-IP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After I added &lt;code&gt;10.0.0.0/8&lt;/code&gt;, the same request passed the filter and failed with &lt;code&gt;ECONNREFUSED&lt;/code&gt; instead. Keep the range as narrow as the addresses you actually use.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Check the logs
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;access.log&lt;/code&gt; in the data folder records only connections that got through, once per IP. Connections the whitelist blocked appear only as &lt;code&gt;Blocked connection from&lt;/code&gt; lines in the console. Basic auth returns 429 for 60 seconds starting from the sixth failure. During that window even the correct password is refused, and the console logs a &lt;code&gt;Basic auth failed: Rate limited from&lt;/code&gt; line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;As of 2026-10-28, a SillyTavern you reach from other devices needs 1.18.0 or later for this year's 11 CVEs, a Node build that has the &lt;code&gt;BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH&lt;/code&gt; constant (v22.23.4, v24.22.1, v26.12.1), and &lt;code&gt;privateAddressWhitelist.enabled: true&lt;/code&gt;. The official Docker &lt;code&gt;latest&lt;/code&gt; image still carried Node 24.21.0 that day, so pulling it again doesn't fix the Node side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;Node updates on Termux (Android) installs: I have no device to run them on. The commands in the table are copied from the official docs.&lt;/p&gt;

&lt;p&gt;The message you get when starting 1.17.0 or later on Node below 20: I didn't check it, because I didn't download a Node 18 binary. I only confirmed from the source that &lt;code&gt;server.js&lt;/code&gt; has no version check.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node -v&lt;/code&gt; inside a container and the Node version of a self-built image: I couldn't check either, because this Mac has no Docker runtime. I used &lt;code&gt;NODE_VERSION&lt;/code&gt; from the image config instead.&lt;/p&gt;

&lt;p&gt;hostWhitelist blocking behavior: I didn't run it. I only confirmed the default value from the source.&lt;/p&gt;

&lt;p&gt;The real effect of a malicious Brotli response on SillyTavern: I deliberately did not reproduce it. I stopped at the headers and the decoder path.&lt;/p&gt;

&lt;p&gt;Brotli decoding paths in extensions and server plugins: I didn't trace them. I only looked at the core &lt;code&gt;src&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Real-world exploitation targeting SillyTavern: I couldn't confirm any. CISA KEV and NVD only say the Brotli decoder was exploited. I couldn't independently confirm claims made in security vendors' posts either.&lt;/p&gt;

&lt;p&gt;The CVSS 3.1 9.8 score for CVE-2026-111497: it appears only on some aggregator sites. NVD doesn't list it, so this post uses CVSS 4.0 9.2.&lt;/p&gt;

&lt;p&gt;The update path for the winget Node.js LTS package: I didn't run it, because that package wasn't installed on my Windows machine. I didn't run the upgrade either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-10-28 first version (version checks on SillyTavern 1.17.0 and 1.19.0 and on six Node versions, four config startups, Docker image metadata, Windows fnm check, fnm switch, NVD and KEV lookup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>node</category>
      <category>selfhosted</category>
      <category>ai</category>
    </item>
    <item>
      <title>ComfyUI MCP servers compared: comfyui-mcp, comfy-mcp and comfyui-mcp-secure in Claude Code, measured for ports, paths, tokens</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:52:15 +0000</pubDate>
      <link>https://dev.to/ilan_kim/comfyui-mcp-servers-compared-comfyui-mcp-comfy-mcp-and-comfyui-mcp-secure-in-claude-code-2a99</link>
      <guid>https://dev.to/ilan_kim/comfyui-mcp-servers-compared-comfyui-mcp-comfy-mcp-and-comfyui-mcp-secure-in-claude-code-2a99</guid>
      <description>&lt;p&gt;If you want the widest feature set, pick &lt;code&gt;comfyui-mcp&lt;/code&gt;; for official support, &lt;code&gt;comfy-mcp&lt;/code&gt;; for the least exposure of your local file system, &lt;code&gt;comfyui-mcp-secure&lt;/code&gt;. Run with their default &lt;code&gt;stdio&lt;/code&gt; transport, none of the three opens a single port, so the real differences are how far each one reads and writes local files and how much model context its tool definitions consume.&lt;/p&gt;

&lt;p&gt;This is for anyone about to connect a local ComfyUI to Claude Code. On 2026-10-28 I installed all three servers at pinned versions and measured them. The client was Claude Code 2.1.312 on macOS 27.0.1, and ComfyUI was 0.35.0 on the same Mac. Only the first-generation run used ComfyUI 0.33.1 on a Windows machine with an RTX 5090. I run this blog's own workflow with several MCP servers attached to Claude Code and Codex.&lt;/p&gt;

&lt;p&gt;The security section is written from the defender's side only: whether your setup is affected, which versions and settings to change, and where to look for traces. Every test ran on the loopback interface of my own two machines and inside freshly created empty folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  comfyui-mcp, comfy-mcp and comfyui-mcp-secure aim at different things
&lt;/h2&gt;

&lt;p&gt;These three are not rivals doing the same job. They split into maximum features, official, and security-focused. The table below shows values I checked on 2026-10-28 through the GitHub API and the package registries; star counts and release dates are as of that day.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Package (measured version)&lt;/th&gt;
&lt;th&gt;Language, license&lt;/th&gt;
&lt;th&gt;Stars&lt;/th&gt;
&lt;th&gt;Last release&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;artokun/comfyui-mcp&lt;/td&gt;
&lt;td&gt;npm &lt;code&gt;comfyui-mcp@0.52.206&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;TypeScript, MIT&lt;/td&gt;
&lt;td&gt;871&lt;/td&gt;
&lt;td&gt;2026-10-14&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comfy-Org/comfy-mcp&lt;/td&gt;
&lt;td&gt;PyPI &lt;code&gt;comfy-mcp==0.10.0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python, AGPL-3.0 OR Commercial&lt;/td&gt;
&lt;td&gt;308&lt;/td&gt;
&lt;td&gt;2026-08-10&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hybridindie/comfyui_mcp&lt;/td&gt;
&lt;td&gt;PyPI &lt;code&gt;comfyui-mcp-secure==2.3.0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python, MIT&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;2026-10-06&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;comfyui-mcp&lt;/code&gt; is the largest community server. It bundles a Claude Code plugin, hooks, and &lt;code&gt;--http&lt;/code&gt; and &lt;code&gt;--tunnel&lt;/code&gt; modes in one package. The README says 38 tools, but the actual handshake returned 41.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comfy-mcp&lt;/code&gt; is the official server that Comfy-Org created in 2026-07. It does not open its own socket to ComfyUI; it shells out to &lt;code&gt;comfy-cli&lt;/code&gt; instead. &lt;strong&gt;PyPI 0.10.0 is 47 commits behind main.&lt;/strong&gt; The README's quick start also installs from a checkout rather than from PyPI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comfyui-mcp-secure&lt;/code&gt; is a small server with 3 stars. Its selling points are workflow inspection, a path sanitizer and an audit log. Peleke/comfyui-mcp was also a candidate, but I dropped it: 2 stars and no pushes since 2026-03-13.&lt;/p&gt;

&lt;h2&gt;
  
  
  With the default stdio transport, none of the three opens a port
&lt;/h2&gt;

&lt;p&gt;I started each server with its defaults and counted LISTEN sockets across its process tree. All three had zero. A &lt;code&gt;stdio&lt;/code&gt; server is spawned by Claude Code as a child process and talks only over pipes. Here is the log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## stdio 기본 — artokun comfyui-mcp@0.52.206
  LISTEN (none)
## stdio 기본 — comfy-mcp 0.10.0
  LISTEN (none)
## stdio 기본 — comfyui-mcp-secure 2.3.0
  LISTEN (none)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The &lt;code&gt;stdio 기본&lt;/code&gt; headers are my log labels and mean "stdio default".)&lt;/p&gt;

&lt;p&gt;Only two of them have an HTTP mode. &lt;code&gt;comfyui-mcp --http&lt;/code&gt; bound to 127.0.0.1:9100. &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; with &lt;code&gt;transport.remote.enabled&lt;/code&gt; bound to 127.0.0.1:8080. &lt;code&gt;comfy-mcp&lt;/code&gt; is hard-coded to &lt;code&gt;stdio&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;&lt;code&gt;comfyui-mcp --http&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;comfyui-mcp-secure&lt;/code&gt; remote&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No token&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unfamiliar Origin header&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No token or wrong token, after setting &lt;code&gt;COMFYUI_MCP_HTTP_TOKEN&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;no such setting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct token, same setting&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;no such setting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;An HTTP mode without a token is an open door, even on loopback.&lt;/strong&gt; The transport section of the MCP specification, 2025-11-25 revision, says servers must validate the Origin header and return 403 when it is wrong. Started without a token, both servers answered 200 to an unfamiliar Origin.&lt;/p&gt;

&lt;p&gt;There is already a case that shows why this matters. CVE-2026-61568 in &lt;code&gt;@zereight/mcp-gitlab&lt;/code&gt; was a flaw that let DNS rebinding reach its local Streamable HTTP transport; it was fixed in 2.1.30. It shows that an HTTP server bound to loopback can still be reached through a browser. I did not test whether the same path works against these two servers.&lt;/p&gt;

&lt;p&gt;One more thing caught me. The default port of &lt;code&gt;comfyui-mcp&lt;/code&gt;, 9100, is one that printers commonly use. On my Mac, a printer driver was already holding &lt;code&gt;*:9100&lt;/code&gt;. Requests to 127.0.0.1:9100 did reach the MCP server, but changing the port with &lt;code&gt;--port&lt;/code&gt; saves you the confusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  comfyui-mcp and comfy-mcp read and write outside ComfyUI, even on the Desktop
&lt;/h2&gt;

&lt;p&gt;I passed a freshly created &lt;code&gt;~/Desktop/mcp-scope-test/&lt;/code&gt; path to the upload and output-path tools listed in the table below. &lt;code&gt;comfyui-mcp&lt;/code&gt; and &lt;code&gt;comfy-mcp&lt;/code&gt; did not refuse it. &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; has no parameter that takes a local path at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Read from an arbitrary path&lt;/th&gt;
&lt;th&gt;Write to an arbitrary path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comfyui-mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;upload_image&lt;/code&gt; allows it, image extensions only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;save_dir&lt;/code&gt; in &lt;code&gt;get_image&lt;/code&gt; allows it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comfy-mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;upload_file&lt;/code&gt; allows it, &lt;code&gt;.txt&lt;/code&gt; passes too&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;out_path&lt;/code&gt; in &lt;code&gt;fetch_template&lt;/code&gt; allows it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comfyui-mcp-secure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no such parameter (takes base64)&lt;/td&gt;
&lt;td&gt;no such parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those two servers read and wrote anywhere the user who launched the MCP server can. I found no setting in their READMEs that confines reads and writes to a root folder. If the model falls for a prompt injection, these path parameters can become the channel.&lt;/p&gt;

&lt;p&gt;The same class of bug has already shipped as a real CVE. In mcp-atlassian, CVE-2026-27825 allowed arbitrary file writes because &lt;code&gt;download_path&lt;/code&gt; was not validated. It was rated CVSS 9.0 critical and fixed in 0.17.0. A tool that accepts an output path sits in the same place as that case.&lt;/p&gt;

&lt;h2&gt;
  
  
  comfyui-mcp has the heaviest tool definitions at 77.3k tokens
&lt;/h2&gt;

&lt;p&gt;I attached the servers one at a time and measured the MCP tools entry with &lt;code&gt;claude -p "/context"&lt;/code&gt;. The figures were 77.3k for &lt;code&gt;comfyui-mcp&lt;/code&gt;, 25.7k for &lt;code&gt;comfy-mcp&lt;/code&gt; and 14.5k for &lt;code&gt;comfyui-mcp-secure&lt;/code&gt;. &lt;code&gt;comfyui-mcp --compact&lt;/code&gt; shrank to 3 tools and 452 tokens.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyhzvlmb4k35rwvqwiex.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdyhzvlmb4k35rwvqwiex.webp" alt="Bar chart of the context tokens taken by each MCP server's tool definitions" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Those numbers are not all loaded on every call, though. Claude Code has tool search on by default, so MCP tool definitions are deferred. The baseline with no MCP server was 15.4k. With default settings, &lt;code&gt;comfyui-mcp&lt;/code&gt; came to 15.9k and &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; to 15.6k, barely more.&lt;/p&gt;

&lt;p&gt;Only &lt;code&gt;comfy-mcp&lt;/code&gt; grew by 1.6k, to 17.0k. That is because the 16,805 characters of instructions the server sends are always loaded. &lt;strong&gt;With &lt;code&gt;ENABLE_TOOL_SEARCH=false&lt;/code&gt;, &lt;code&gt;comfyui-mcp&lt;/code&gt; brings the total to 109.9k.&lt;/strong&gt; If you run with tool search off, &lt;code&gt;--compact&lt;/code&gt; is practically mandatory.&lt;/p&gt;

&lt;p&gt;I also read the tool descriptions by hand to look for hidden instructions that steer the model. Every imperative phrase was guidance about the order of use. &lt;code&gt;comfyui-mcp&lt;/code&gt; and &lt;code&gt;comfy-mcp&lt;/code&gt; even include a sentence telling the model not to trust externally sourced content as instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install size differs tenfold: 851MB versus 82MB
&lt;/h2&gt;

&lt;p&gt;I installed each server at a pinned version into an empty directory and measured the size. By &lt;code&gt;du -sh&lt;/code&gt;, &lt;code&gt;comfyui-mcp&lt;/code&gt; came to 192 packages and 851MB including optional dependencies. Leaving those out with &lt;code&gt;--omit=optional&lt;/code&gt; brought it down to 109 packages and 88MB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9itqgp6yzv3qfm9x9w2r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9itqgp6yzv3qfm9x9w2r.webp" alt="Bar chart of the install size of each MCP server" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The largest optional dependency is &lt;code&gt;@openai/codex&lt;/code&gt; at 323MB, followed by &lt;code&gt;@anthropic-ai/claude-agent-sdk&lt;/code&gt; at 291MB and &lt;code&gt;cloudflared&lt;/code&gt; at 38MB. They exist for the sidebar panel and the tunnel feature. The &lt;code&gt;--omit=optional&lt;/code&gt; install still exposed all 41 tools. &lt;code&gt;comfy-mcp&lt;/code&gt; came to 171MB including &lt;code&gt;comfy-cli&lt;/code&gt; 1.23.0, and &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; to 82MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cloudflared&lt;/code&gt; downloads its binary at install time even if you never use &lt;code&gt;--tunnel&lt;/code&gt;.&lt;/strong&gt; npm 11.19.0 only warned that the postinstall script was not in allowScripts, and the script ran anyway. What it fetched was the unpinned latest release. I found no hash check in the code.&lt;/p&gt;

&lt;p&gt;Every README example also uses &lt;code&gt;npx -y comfyui-mcp@latest&lt;/code&gt;, which fetches and launches whatever is newest at that moment, each time it runs. &lt;code&gt;comfy-mcp&lt;/code&gt; does not declare &lt;code&gt;comfy-cli&lt;/code&gt; as a dependency, so I had to install it separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  comfyui-mcp reached the first image fastest: 12 seconds, 4 tool calls
&lt;/h2&gt;

&lt;p&gt;I asked each of the three servers for one SDXL image with the same prompt. The checkpoint was &lt;code&gt;sd_xl_base_1.0.safetensors&lt;/code&gt;, the seed 20261028, the size 1024×1024. ComfyUI was the 0.33.1 instance on the RTX 5090 Windows machine, attached to the Mac's loopback through an SSH tunnel. The agent was &lt;code&gt;claude -p&lt;/code&gt; running Sonnet 5.5.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Tool calls (incl. ToolSearch)&lt;/th&gt;
&lt;th&gt;Wall clock&lt;/th&gt;
&lt;th&gt;How the image was made&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comfyui-mcp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;12 s&lt;/td&gt;
&lt;td&gt;one &lt;code&gt;generate_image&lt;/code&gt; call with every value set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;comfy-mcp&lt;/code&gt; + &lt;code&gt;COMFYUI_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;failure&lt;/td&gt;
&lt;td&gt;9 (3 errors)&lt;/td&gt;
&lt;td&gt;37 s&lt;/td&gt;
&lt;td&gt;slot tools looked at the default 8188&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;comfy-mcp&lt;/code&gt; + &lt;code&gt;COMFY_LOCAL_URL&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;89 s&lt;/td&gt;
&lt;td&gt;fetched a template and edited 12 slots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;comfyui-mcp-secure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;14 s&lt;/td&gt;
&lt;td&gt;wrote an API-format workflow directly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The images from the three servers were identical down to the pixel.&lt;/strong&gt; In the end, each one fed the same graph to ComfyUI. The difference lay in the tool calls it took to build that graph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xhhpgvsgw2s2vuvevnb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xhhpgvsgw2s2vuvevnb.webp" alt="The SDXL first-generation image the three MCP servers produced with identical settings, a red lighthouse on a coast" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Image: AI-generated (Stable Diffusion XL &lt;code&gt;sd_xl_base_1.0&lt;/code&gt;, the output of this post's first-generation run).&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;comfyui-mcp&lt;/code&gt;, a single &lt;code&gt;generate_image&lt;/code&gt; takes seed, sampler and scheduler. The agent submitted, checked the status once and read the result, and that was all. &lt;code&gt;comfyui_generate_image&lt;/code&gt; in &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; takes neither seed nor sampler, so the agent wrote the workflow itself and submitted it with &lt;code&gt;comfyui_run_workflow&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The audit log of &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; recorded the list of nodes it inspected before submission, with the verdict &lt;code&gt;allowed&lt;/code&gt;. In the default &lt;code&gt;audit&lt;/code&gt; mode it only records risky nodes and does not block them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comfy-mcp&lt;/code&gt; failed on the first attempt. When I pointed the remote-oriented &lt;code&gt;COMFYUI_URL&lt;/code&gt; at the tunnel, &lt;code&gt;list_workflow_slots&lt;/code&gt; returned nothing but &lt;code&gt;Error executing tool&lt;/code&gt;. Running comfy-cli directly with the same setting gave &lt;code&gt;cannot reach http://127.0.0.1:8188/object_info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For another port on the same machine, such as an SSH tunnel, set only &lt;code&gt;COMFY_LOCAL_URL&lt;/code&gt;.&lt;/strong&gt; The README's &lt;code&gt;Which address variable do I want?&lt;/code&gt; section spells out this distinction. &lt;code&gt;COMFYUI_URL&lt;/code&gt; moves only the submission and job tools, and the remaining tools keep looking at the default 8188. After switching to &lt;code&gt;COMFY_LOCAL_URL&lt;/code&gt;, it succeeded in 89 seconds.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;generate_image&lt;/code&gt; tool in &lt;code&gt;comfy-mcp&lt;/code&gt; accepts only &lt;code&gt;checkpoint&lt;/code&gt; and &lt;code&gt;prompt&lt;/code&gt;. The only way to pin the seed was to edit template slots, and that back-and-forth is why the tool calls climbed to 14. ComfyUI's execution took 10.6 seconds on the first run. Later runs hit the cache because the graph was the same, so I did not compare GPU time.&lt;/p&gt;

&lt;p&gt;The default template of &lt;code&gt;generate_image&lt;/code&gt; also differed from the docs. The &lt;code&gt;comfy-mcp&lt;/code&gt; 0.10.0 README says &lt;code&gt;generate_image&lt;/code&gt; runs the SD1.5 &lt;code&gt;default&lt;/code&gt; template. The installed package's code uses &lt;code&gt;image_z_image_turbo&lt;/code&gt; instead, and a code comment says &lt;code&gt;default&lt;/code&gt; was removed from the gallery.&lt;/p&gt;

&lt;p&gt;Indeed, passing &lt;code&gt;default&lt;/code&gt; to &lt;code&gt;fetch_template&lt;/code&gt; produced &lt;code&gt;Error executing tool fetch_template&lt;/code&gt;. &lt;code&gt;image_z_image_turbo&lt;/code&gt; requires a qwen-family text encoder and the z-image model. &lt;strong&gt;If all you have is an SDXL checkpoint, switch the template with &lt;code&gt;COMFY_T2I_TEMPLATE&lt;/code&gt;.&lt;/strong&gt; You also need the environment variables that match the slot names; the code comment gives &lt;code&gt;COMFY_T2I_CHECKPOINT_SLOT&lt;/code&gt; as an example.&lt;/p&gt;

&lt;h2&gt;
  
  
  An MCP server security checklist in four steps
&lt;/h2&gt;

&lt;p&gt;Here is how the three servers measure up across six areas first. Each cell is a value I confirmed on 2026-10-28 by measurement or by reading the code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;&lt;code&gt;comfyui-mcp&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;comfy-mcp&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;comfyui-mcp-secure&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport and auth&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stdio&lt;/code&gt; default, HTTP token optional&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stdio&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;stdio&lt;/code&gt; default, no HTTP auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path scope&lt;/td&gt;
&lt;td&gt;reads and writes arbitrary paths&lt;/td&gt;
&lt;td&gt;reads and writes arbitrary paths&lt;/td&gt;
&lt;td&gt;no local path parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confirmation for risky actions&lt;/td&gt;
&lt;td&gt;no server-side confirmation&lt;/td&gt;
&lt;td&gt;user confirmation for 6 actions such as node installs&lt;/td&gt;
&lt;td&gt;watches 159 risky nodes, warns only by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context&lt;/td&gt;
&lt;td&gt;77.3k, 452 with &lt;code&gt;--compact&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;25.7k + instructions&lt;/td&gt;
&lt;td&gt;14.5k&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secrets and audit&lt;/td&gt;
&lt;td&gt;returns ComfyUI launch arguments&lt;/td&gt;
&lt;td&gt;returns launch arguments&lt;/td&gt;
&lt;td&gt;whitelisted fields only, audit log per call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supply chain&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@latest&lt;/code&gt; examples, &lt;code&gt;cloudflared&lt;/code&gt; downloaded without a hash&lt;/td&gt;
&lt;td&gt;PyPI 47 commits behind main&lt;/td&gt;
&lt;td&gt;unpinned &lt;code&gt;uvx&lt;/code&gt; examples&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three had zero GitHub security advisories. No advisories does not mean no flaws, so check your own setup in the order below.&lt;/p&gt;

&lt;h3&gt;
  
  
  First, check whether your setup is affected
&lt;/h3&gt;

&lt;p&gt;Start with which servers are attached and which transport they use. If you turned on an HTTP mode, check for open ports.&lt;/p&gt;

&lt;p&gt;List the servers you have attached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the ports MCP servers have opened. If you only use &lt;code&gt;stdio&lt;/code&gt;, no server process should show up here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsof &lt;span class="nt"&gt;-nP&lt;/span&gt; &lt;span class="nt"&gt;-iTCP&lt;/span&gt; &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then search your config files for &lt;code&gt;--http&lt;/code&gt;, &lt;code&gt;--tunnel&lt;/code&gt; and &lt;code&gt;transport.remote.enabled&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pin patches and versions
&lt;/h3&gt;

&lt;p&gt;Write down the version you checked instead of &lt;code&gt;@latest&lt;/code&gt; or an unpinned &lt;code&gt;uvx&lt;/code&gt;. Upgrade only after reading the release notes.&lt;/p&gt;

&lt;p&gt;Register npm servers with an explicit version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add comfyui &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; comfyui-mcp@0.52.206
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin Python servers as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"comfyui-mcp-secure==2.3.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you install &lt;code&gt;comfyui-mcp&lt;/code&gt; yourself, use &lt;code&gt;--omit=optional&lt;/code&gt; to avoid the &lt;code&gt;cloudflared&lt;/code&gt; download.&lt;/p&gt;

&lt;h3&gt;
  
  
  Narrow the settings
&lt;/h3&gt;

&lt;p&gt;Turn off what you don't use to shrink the exposed surface. All of the following are settings the servers already provide.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you really need an HTTP mode, set &lt;code&gt;COMFYUI_MCP_HTTP_TOKEN&lt;/code&gt; for &lt;code&gt;comfyui-mcp&lt;/code&gt;. The HTTP mode of &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; has no built-in auth, so you are better off using it over &lt;code&gt;stdio&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;comfyui-mcp-secure&lt;/code&gt;, change &lt;code&gt;security.mode&lt;/code&gt; in &lt;code&gt;~/.comfyui-mcp/config.yaml&lt;/code&gt; to &lt;code&gt;enforce&lt;/code&gt; and list the nodes you use under &lt;code&gt;allowed_nodes&lt;/code&gt;. According to the code, an empty list blocks nothing even in &lt;code&gt;enforce&lt;/code&gt;. The default &lt;code&gt;audit&lt;/code&gt; only records, and I did not run the blocking behavior.&lt;/li&gt;
&lt;li&gt;If you use the panel feature of &lt;code&gt;comfyui-mcp&lt;/code&gt;, lower &lt;code&gt;COMFYUI_MCP_CODEX_SANDBOX&lt;/code&gt; to &lt;code&gt;read-only&lt;/code&gt; or &lt;code&gt;workspace-write&lt;/code&gt;. The default in the code is &lt;code&gt;danger-full-access&lt;/code&gt;, and the README does not mention that default.&lt;/li&gt;
&lt;li&gt;Run ComfyUI on 127.0.0.1 only, without &lt;code&gt;--listen&lt;/code&gt;. Don't put secrets in ComfyUI's launch arguments: &lt;code&gt;comfyui-mcp&lt;/code&gt; and &lt;code&gt;comfy-mcp&lt;/code&gt; pass the launch arguments from &lt;code&gt;/system_stats&lt;/code&gt; straight to the model.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Check for traces
&lt;/h3&gt;

&lt;p&gt;Where a record of what happened ends up differs by server.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;comfyui-mcp-secure&lt;/code&gt; writes one JSON line per call to &lt;code&gt;~/.comfyui-mcp/audit.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I found no built-in audit log in &lt;code&gt;comfyui-mcp&lt;/code&gt; or &lt;code&gt;comfy-mcp&lt;/code&gt;. Look for unfamiliar files uploaded to ComfyUI's &lt;code&gt;input&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;Files written through &lt;code&gt;save_dir&lt;/code&gt; or &lt;code&gt;out_path&lt;/code&gt; land outside the ComfyUI folder. Search your home folder for recently created images and JSON files.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Which server to pick depends on how you work
&lt;/h2&gt;

&lt;p&gt;If you want an agent to drive ComfyUI deeply, &lt;code&gt;comfyui-mcp&lt;/code&gt; does the most. In exchange, pair it with an &lt;code&gt;--omit=optional&lt;/code&gt; install, a pinned version and &lt;code&gt;stdio&lt;/code&gt;. If you run with tool search off, add &lt;code&gt;--compact&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;If you want official support and user confirmation gates, choose &lt;code&gt;comfy-mcp&lt;/code&gt;. When connecting through a tunnel or another port, get &lt;code&gt;COMFY_LOCAL_URL&lt;/code&gt; right first. Before actions such as installing nodes or exposing the network, the server asks for confirmation through MCP elicitation. Note, though, that the PyPI release has been frozen since 2026-08-10, and the license is a dual choice between AGPL-3.0 and a commercial license.&lt;/p&gt;

&lt;p&gt;If you would rather never hand local paths to MCP tools, &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; fits. It does still have tools for installing nodes and downloading models, so files on the ComfyUI side can change. &lt;strong&gt;It is the only one of the three with no local path parameters and an audit log.&lt;/strong&gt; It is a small 3-star project, so keep an eye on whether maintenance continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;All three servers open no port under default &lt;code&gt;stdio&lt;/code&gt;, but &lt;code&gt;comfyui-mcp&lt;/code&gt; and &lt;code&gt;comfy-mcp&lt;/code&gt; read and write arbitrary local paths while &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; takes none and keeps an audit log. Pin versions, keep &lt;code&gt;stdio&lt;/code&gt; (or set a token for HTTP), install &lt;code&gt;comfyui-mcp&lt;/code&gt; with &lt;code&gt;--omit=optional&lt;/code&gt;, and add &lt;code&gt;--compact&lt;/code&gt; if you turn tool search off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;Tunnel and external exposure modes: I did not run &lt;code&gt;comfyui-mcp&lt;/code&gt;'s &lt;code&gt;--tunnel&lt;/code&gt; or any non-loopback bind. Exposing a server to the outside is out of scope for this post, so for those modes I only read the README and the &lt;code&gt;--help&lt;/code&gt; text.&lt;/p&gt;

&lt;p&gt;MCP servers launched on Windows: I ran all three servers on macOS only. Path handling and port conflicts may behave differently on Windows.&lt;/p&gt;

&lt;p&gt;Unknown vulnerabilities in the three servers: zero GitHub security advisories only means no advisory has been published. I did not audit the full code.&lt;/p&gt;

&lt;p&gt;Secondary MCP CVE tallies: figures such as "30 MCP CVEs in 60 days" come from secondary sources, so I left them out of the body. The cases in the body are only CVEs I checked directly in GitHub Advisory and OSV.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;comfyui-mcp&lt;/code&gt; refusing unauthenticated non-loopback binds: &lt;code&gt;--help&lt;/code&gt; says it refuses an unauthenticated non-loopback bind unless &lt;code&gt;--allow-unauthenticated-non-loopback&lt;/code&gt; is given. I did not test a non-loopback bind, so I could not confirm that refusal.&lt;/p&gt;

&lt;p&gt;The relative-path sanitizer: the &lt;code&gt;comfyui-mcp&lt;/code&gt; tool descriptions and the &lt;code&gt;comfyui-mcp-secure&lt;/code&gt; README say parent paths are rejected. That describes relative names inside the ComfyUI folder. I did not test parent-path strings.&lt;/p&gt;

&lt;p&gt;Real behavior of the &lt;code&gt;comfyui-mcp&lt;/code&gt; panel orchestrator: I confirmed the orchestrator's permission default in the code only. I did not launch the feature to see whether it actually runs with that permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-10-28 first version (macOS Claude Code 2.1.312; ports, paths, context, install size and an RTX 5090 first-generation run measured for all three servers)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>comfyui</category>
      <category>mcp</category>
      <category>claudecode</category>
      <category>security</category>
    </item>
    <item>
      <title>NovelAI V5 upscaling measured: Enhance at 35 and 45 Anlas vs ComfyUI Anima i2i, and how much the style shifts (ΔE)</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Thu, 01 Oct 2026 04:11:06 +0000</pubDate>
      <link>https://dev.to/ilan_kim/novelai-v5-upscaling-measured-enhance-at-35-and-45-anlas-vs-comfyui-anima-i2i-and-how-much-the-2kl0</link>
      <guid>https://dev.to/ilan_kim/novelai-v5-upscaling-measured-enhance-at-35-and-45-anlas-vs-comfyui-anima-i2i-and-how-much-the-2kl0</guid>
      <description>&lt;p&gt;NovelAI's Enhance changed V5 images more than a local Anima i2i pass in ComfyUI did: the mean color difference from the original (ΔE2000) was between 10 and 15 for Enhance, and between 8.7 and 10.4 for Anima i2i even at denoise 0.55. If you only want a bigger image, the standalone Upscale at 1 Anlas stayed closest to the original.&lt;/p&gt;

&lt;p&gt;This is for anyone who has hesitated over which route to take when enlarging a V5 image. On 2026-10-01 I prepared three originals with &lt;code&gt;nai-diffusion-5-full&lt;/code&gt;, ran them through NAI Enhance 1.5×, Enhance Max and the standalone Upscale, and then ran the same originals through 15 ComfyUI jobs on an RTX 5090. A few terms if NovelAI is new to you: Anlas is NovelAI's generation credit, Enhance is the web UI option that enlarges an image and redraws it, and Anima is the model I run locally in ComfyUI. I usually generate in NovelAI first and move to Anima when I get stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance 1.5× cost 35 Anlas, Max cost 45 Anlas
&lt;/h2&gt;

&lt;p&gt;Even on Opus, NovelAI's top subscription tier, Enhance and Upscale are not free. One of the conditions for free generation is that no other image is used as a base, so feeding in an existing picture takes Anlas off your balance (subscription page checked 2026-10-01).&lt;/p&gt;

&lt;p&gt;I ran each action in the web UI and checked the balance after every one. Generating the originals cost 0 Anlas, as in &lt;a href="https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l"&gt;the earlier post&lt;/a&gt;. Here is what each action deducted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Output size (portrait original)&lt;/th&gt;
&lt;th&gt;Anlas deducted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original generation 832×1216, 28 steps&lt;/td&gt;
&lt;td&gt;832×1216&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standalone Upscale&lt;/td&gt;
&lt;td&gt;1664×2432&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enhance 1.5×&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enhance Max&lt;/td&gt;
&lt;td&gt;1467×2144&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two portrait images and the one landscape image all cost the same. That is higher than the 30 and 38 written in a community tool. The button in the Enhance window also showed 35 and 45, so look at that number before you click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance output records a strength 0.5 i2i request
&lt;/h2&gt;

&lt;p&gt;Enhance output PNGs carry the request that produced them. NovelAI hides generation data inside its PNGs (the "stealth metadata" covered further down), and opening it shows the request as it was sent. Both levels were an &lt;code&gt;Img2ImgRequest&lt;/code&gt; with strength 0.5, noise 0 and 28 steps. The two levels differed like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Enhance 1.5×&lt;/th&gt;
&lt;th&gt;Enhance Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;i2i request resolution&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;832×1216 (original size)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final output&lt;/td&gt;
&lt;td&gt;1280×1856&lt;/td&gt;
&lt;td&gt;1467×2144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;End of prompt&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-2::upscaled, blurry::,&lt;/code&gt; appended automatically&lt;/td&gt;
&lt;td&gt;nothing appended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed&lt;/td&gt;
&lt;td&gt;new random value every time&lt;/td&gt;
&lt;td&gt;new random value every time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Character prompts&lt;/td&gt;
&lt;td&gt;carried over from the original&lt;/td&gt;
&lt;td&gt;carried over from the original&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The i2i request size was the enlarged size for 1.5× and the original size for Max.&lt;/strong&gt; Max also had &lt;code&gt;upscaled_enhance: true&lt;/code&gt; recorded separately, so Max appears to enlarge the image after the i2i pass.&lt;/p&gt;

&lt;p&gt;Metadata alone cannot settle the processing order inside the server, so I left that as unverified. Magnitude 2 in the Enhance window is the same value as Strength 0.5.&lt;/p&gt;

&lt;p&gt;The seed changes every time you click. Feeding in the same original again gives a different result, so save a result you like right away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance changed the image more than Anima i2i at 0.55
&lt;/h2&gt;

&lt;p&gt;The reference is the original enlarged to the output size with Lanczos. ΔE2000 looks at color difference, and grayscale SSIM looks at how much of the lines and shapes survive. The values are the range across the three originals:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Scale&lt;/th&gt;
&lt;th&gt;Mean ΔE&lt;/th&gt;
&lt;th&gt;SSIM&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAI standalone Upscale&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;0.85~1.04&lt;/td&gt;
&lt;td&gt;0.966~0.981&lt;/td&gt;
&lt;td&gt;1 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ESRGAN (&lt;code&gt;RealESRGAN_x4plus_anime_6B&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;2.07~2.59&lt;/td&gt;
&lt;td&gt;0.904~0.931&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.25&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;4.33~5.72&lt;/td&gt;
&lt;td&gt;0.717~0.790&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.40&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;6.29~7.90&lt;/td&gt;
&lt;td&gt;0.615~0.700&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.55&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;8.69~10.44&lt;/td&gt;
&lt;td&gt;0.555~0.631&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i denoise 0.40&lt;/td&gt;
&lt;td&gt;2×&lt;/td&gt;
&lt;td&gt;5.38~6.95&lt;/td&gt;
&lt;td&gt;0.693~0.765&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAI Enhance 1.5×&lt;/td&gt;
&lt;td&gt;1.5×&lt;/td&gt;
&lt;td&gt;10.63~15.43&lt;/td&gt;
&lt;td&gt;0.481~0.606&lt;/td&gt;
&lt;td&gt;35 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NAI Enhance Max&lt;/td&gt;
&lt;td&gt;about 1.76×&lt;/td&gt;
&lt;td&gt;10.19~14.29&lt;/td&gt;
&lt;td&gt;0.498~0.638&lt;/td&gt;
&lt;td&gt;45 Anlas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;By the numbers, Enhance changed the image the most. Apart from the background image, saturation changed by 0.03 or less. SSIM fell at the same time, so the lines and shapes changed too. The exception is Enhance 1.5× on the background image, where saturation rose by 0.10 and the blue channel by 19.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wclasak0tgtk1le5o19.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wclasak0tgtk1le5o19.webp" alt="Face crops of the original and the NAI Upscale, Enhance 1.5×, Enhance Max, Anima i2i 0.40 and 0.55 results, cut at the same position and placed side by side" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cropping the face shows the same order. Upscale is hard to tell apart from the original. Enhance 1.5× redrew the bangs and the eye highlights. Max changed even the eye shape and the irises. Anima i2i 0.40 kept the outlines closer to the original.&lt;/p&gt;

&lt;p&gt;The usual community answer is that moving to a local model changes the style because the checkpoint is different. I had taken that on trust and assumed Enhance would be the safe option. Yet Enhance, which uses the same V5 model, changed the image just as much. Even with the same model, an i2i pass changes this much.&lt;/p&gt;

&lt;h2&gt;
  
  
  In the two-character image, Enhance kept the eye color and Anima changed it
&lt;/h2&gt;

&lt;p&gt;The difference was clearest on the two-character original, which I generated with the prompt split across two character prompt slots. The blue-haired character with glasses has blue eyes in the original. Enhance kept the blue eyes at both levels. Anima i2i turned them green from 0.40 on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwvp0veez9fp9t18a6lr.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbwvp0veez9fp9t18a6lr.webp" alt="Face crops of the blue-haired character from the two-character original compared across six methods; only the two Anima i2i panels have green eyes" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Green is the other character's eye color. The glasses frames also went from black to gold. On the Anima side I used core nodes only, so both characters' attributes were written on a single line.&lt;/p&gt;

&lt;p&gt;That line contains only &lt;code&gt;green eyes&lt;/code&gt;, not &lt;code&gt;blue eyes&lt;/code&gt;. The NAI character slots did not have &lt;code&gt;blue eyes&lt;/code&gt; either. This one image cannot tell whether the cause is the single-line prompt or the difference between models.&lt;/p&gt;

&lt;p&gt;The Enhance request carried both character slots unchanged. The glasses frames, though, became thick and square, and Max also changed the shape of the mouth. In this one image, the eye color survived and the details were redrawn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign lettering was rewritten by every i2i pass
&lt;/h2&gt;

&lt;p&gt;On the rainy neon street, no i2i pass kept the lettering. Enhance changed the layout and colors of the signs wholesale. Anima i2i 0.40 kept part of the large sign's shape, and at 0.55 that changed as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhbewwhgf3l0k7mj9uqn.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhbewwhgf3l0k7mj9uqn.webp" alt="Crops of the neon-sign street original and the six methods' results at the same position; every i2i result has different sign lettering" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most of the sign lettering in the original is meaningless characters anyway. It is still a problem if you liked the sign design, and in that case you should use only Upscale or ESRGAN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local ComfyUI post-processing on an RTX 5090: 2 to 38 seconds per image, 0 Anlas
&lt;/h2&gt;

&lt;p&gt;The local setup is ComfyUI 0.33.1 on Windows 11, with torch 2.11.0+cu128 and an RTX 5090 32GB. I used core nodes only, with no custom nodes, and queued the 15 jobs one after another through the HTTP API (&lt;code&gt;POST /prompt&lt;/code&gt;). Run times per job type were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Pipeline&lt;/th&gt;
&lt;th&gt;Run time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ESRGAN 2×&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;UpscaleModelLoader&lt;/code&gt; 4×, then Lanczos to 2× size&lt;/td&gt;
&lt;td&gt;1.63~1.66 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i 1.5×&lt;/td&gt;
&lt;td&gt;Lanczos 1.5×, then &lt;code&gt;VAEEncode&lt;/code&gt; and &lt;code&gt;KSampler&lt;/code&gt; 30 steps&lt;/td&gt;
&lt;td&gt;17.77~19.17 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anima i2i 2×&lt;/td&gt;
&lt;td&gt;Lanczos 2×, then the same i2i&lt;/td&gt;
&lt;td&gt;37.73~38.28 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anima ran at cfg 4.5 with &lt;code&gt;er_sde&lt;/code&gt; and &lt;code&gt;simple&lt;/code&gt;. The seed was 20261001, with no artist tags. The first run took 21 seconds because of model loading, so I left it out. The times are for this one GPU.&lt;/p&gt;

&lt;p&gt;Anima is the only local model I kept. Its weights are non-commercial, but the images it generates can be used commercially; the Anima README says so. NoobAI-XL, which is on the same PC, prohibits commercial use of its outputs, so I left it out (checked 2026-10-01).&lt;/p&gt;

&lt;h2&gt;
  
  
  Stealth metadata disappears on a ComfyUI save but survives webp
&lt;/h2&gt;

&lt;p&gt;NovelAI PNGs hide the prompt in the alpha channel, with the settings stored alongside it in the least significant bits. This is what NovelAI's stealth metadata is. I reimplemented the logic of the official reader and read the file at each stage:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NAI original PNG, Upscale and Enhance output&lt;/td&gt;
&lt;td&gt;survives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS &lt;code&gt;sips&lt;/code&gt; PNG re-save&lt;/td&gt;
&lt;td&gt;survives (text chunks are lost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lossless webp, lossy q90 webp (Pillow, sharp)&lt;/td&gt;
&lt;td&gt;survives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ComfyUI &lt;code&gt;SaveImage&lt;/code&gt; PNG&lt;/td&gt;
&lt;td&gt;lost (saved as RGB, no alpha)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JPEG conversion&lt;/td&gt;
&lt;td&gt;lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Half-size resize (one original)&lt;/td&gt;
&lt;td&gt;lost (&lt;code&gt;magic not found&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you only convert to webp and upload, the full prompt goes along with it.&lt;/strong&gt; It read back intact even from a q90 lossy webp.&lt;/p&gt;

&lt;p&gt;The original PNG also has the prompt in its &lt;code&gt;Comment&lt;/code&gt; and &lt;code&gt;Description&lt;/code&gt; text chunks. To hide it, strip both the alpha channel and the text chunks, then read the final file again before uploading to confirm.&lt;/p&gt;

&lt;p&gt;One pass through ComfyUI and the NAI metadata is gone. In its place, &lt;code&gt;SaveImage&lt;/code&gt; writes the workflow as a &lt;code&gt;prompt&lt;/code&gt; chunk. If you need the NAI generation info, keep the original PNG separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose by how much of the original you want to keep
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you only want to enlarge the image as it is, the NAI standalone Upscale stays closest. If you have a local GPU, ESRGAN also stays around ΔE 2.&lt;/li&gt;
&lt;li&gt;If there are several characters and you don't mind the details being redrawn, look at Enhance first. In this round's single two-character image, it kept the eye color. The result is different every time, so save it right away.&lt;/li&gt;
&lt;li&gt;To save Anlas, start Anima i2i at denoise 0.25 to 0.40. With several characters, check the result with a crop.&lt;/li&gt;
&lt;li&gt;For backgrounds where signs or lettering matter, avoid i2i and use only Upscale or ESRGAN.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All the pictures in this post were made with AI. Images: AI-generated (NovelAI Diffusion V5, ComfyUI Anima; cover image OpenAI Images).&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;NAI Enhance (35 or 45 Anlas) redrew V5 images more than local Anima i2i at denoise 0.55, while the 1 Anlas standalone Upscale and ESRGAN stayed closest to the original. Every i2i pass rewrote the sign lettering, and a webp conversion keeps the prompt in the stealth metadata, so strip it before you post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;The internal processing order of Enhance: I could not confirm it. The metadata only records the request size and the &lt;code&gt;upscaled_enhance&lt;/code&gt; value, and it does not show at which stage the server enlarges the image.&lt;/p&gt;

&lt;p&gt;Seed-to-seed variance of Enhance results: I did not measure it. There is only one result per setting, so I don't know how much ΔE moves across repeated runs.&lt;/p&gt;

&lt;p&gt;Enhance strengths other than Magnitude 2: I did not run them. I used only the default of 2 and did not check the price or the amount of change at other strengths.&lt;/p&gt;

&lt;p&gt;Processing time on the NAI side: I did not measure it. A person clicked one image at a time, so all I have is a rough sense of the time, and queue waits were mixed in, so I kept it out of the table.&lt;/p&gt;

&lt;p&gt;The Curated model and other local models: out of scope. The originals used only &lt;code&gt;nai-diffusion-5-full&lt;/code&gt;, and locally I ran only Anima.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-10-01 first version (9 NAI Enhance 1.5×, Max and Upscale runs plus 15 ComfyUI jobs, measuring Anlas, ΔE, SSIM and metadata)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>comfyui</category>
      <category>stablediffusion</category>
      <category>ai</category>
    </item>
    <item>
      <title>Crack cash payouts: the 8.8% is other-income tax, not a fee — what you receive, May filing, business registration</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:44:45 +0000</pubDate>
      <link>https://dev.to/ilan_kim/crack-cash-payouts-the-88-is-other-income-tax-not-a-fee-what-you-receive-may-filing-5gd5</link>
      <guid>https://dev.to/ilan_kim/crack-cash-payouts-the-88-is-other-income-tax-not-a-fee-what-you-receive-may-filing-5gd5</guid>
      <description>&lt;p&gt;When you convert Crack cash to money, the 8.8% that disappears is not a fee: it is income tax withheld on "other income" (기타소득) under Korea's Income Tax Act, so converting KRW 1,000,000 leaves KRW 912,000 in your bank account. If you have little other income, you can get most of it back by filing Korea's comprehensive income tax return in May.&lt;/p&gt;

&lt;p&gt;Crack (크랙) is a Korean AI character-chat platform run by Wrtn (뤼튼); creators earn "cash" there when users spend paid Crackers on their works. I wrote this for creators who are building up cash on Crack. I've worked on payment and settlement code as a backend developer. On 2026-09-30 I saved the original text of Crack's announcement and cash guide, then recomputed the 8.8% from the articles of the Income Tax Act. I also read the Crack cash screen on my own account and the tax calculation in Crack's public web code. This is not tax advice. Your actual tax depends on your own income mix and the facts of your situation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 8.8% is withholding tax on other income
&lt;/h2&gt;

&lt;p&gt;Other income (기타소득) is the Korean tax category for irregular income such as lecture fees, and the payer withholds tax before paying you. The 8.8% is exactly what the other-income withholding formula produces. &lt;strong&gt;60% of the payment is deducted as deemed necessary expenses, and the remaining 40% is taxed at 20%.&lt;/strong&gt; Local income tax of 10% of that is added on top.&lt;/p&gt;

&lt;p&gt;Here is the calculation for a KRW 1,000,000 conversion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Conversion           KRW 1,000,000
Other-income amount = 1,000,000 × (1 − 0.6) = 400,000   Income Tax Act Enforcement Decree Art. 87(1-2)
Income tax          = 400,000 × 20%          =  80,000   Income Tax Act Art. 129(1)6(d)
Local income tax    = 80,000 × 10%           =   8,000   Local Tax Act Art. 103-13(1)
Total KRW 88,000 = 8.8% of the payment, deposit KRW 912,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Korean, those citations are 소득세법 시행령 §87 1의2, 소득세법 §129①6라 and 지방세법 §103의13①.&lt;/p&gt;

&lt;p&gt;Crack's own documents point the same way. The screen where you register your conversion details carries a personal-data collection notice. As its legal basis it cites Income Tax Act Articles 21, 127, 145 and 164. Article 21 defines other income, and Article 145 sets out how other income is withheld. Article 144, the withholding provision for business income, is not on the list.&lt;/p&gt;

&lt;p&gt;The Crack Originals notice is more direct. On payouts to original authors, it says they are "transferred to your account after 8.8% other-income tax withholding" ("기타소득세 8.8% 원천징수 후 계좌로 이체"). The 3.3% that Korean freelancers usually see deducted is business-income withholding. Crack cash is handled differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Converting KRW 1,000,000 puts KRW 912,000 in your account
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What lands in your account is the conversion amount minus 8.8%.&lt;/strong&gt; The rate at which cash accrues depends on your work's rating and whether it is registered for Crack Only. Your paid Cracker usage is multiplied by that rate, and the resulting cash arrives on the 1st of the following month.&lt;/p&gt;

&lt;p&gt;As an example, I took a work with 100 million paid Crackers used in a month. I picked a large number to reach the range where the deduction actually bites. I assumed all accrued cash is converted at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Accrual rate&lt;/th&gt;
&lt;th&gt;Cash earned (KRW)&lt;/th&gt;
&lt;th&gt;8.8% withheld (KRW)&lt;/th&gt;
&lt;th&gt;Deposit (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Regular creator, Safety&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;td&gt;912,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Unsafety (from 9/1)&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;td&gt;912,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Safety&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;1,500,000&lt;/td&gt;
&lt;td&gt;132,000&lt;/td&gt;
&lt;td&gt;1,368,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Unsafety + Crack Only&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;td&gt;2,000,000&lt;/td&gt;
&lt;td&gt;176,000&lt;/td&gt;
&lt;td&gt;1,824,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official creator, Safety + Crack Only&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;td&gt;2,500,000&lt;/td&gt;
&lt;td&gt;220,000&lt;/td&gt;
&lt;td&gt;2,280,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Crack Only started on 2026-08-12. Only official creators' works can apply (per the notice as of 2026-09-30). In exchange for publishing a work exclusively on Crack for 18 months, the rate goes up by 1 percentage point. Each series has a cumulative payout cap of KRW 1 billion, counted afresh from September 1.&lt;/p&gt;

&lt;p&gt;The conversion screen calculates the same way. In the public web code, the amount you enter is multiplied by a &lt;code&gt;taxRate&lt;/code&gt; supplied by the server. Fractions of a won are dropped. The screen shows the requested payout amount (지급 요청 금액) and the tax deduction (세금 공제). Below those come the expected payout amount (예상 지급 금액) and expected payout date (지급 예상일). The guide adds that the tax calculation on the screen is an example.&lt;/p&gt;

&lt;p&gt;My account holds 0 cash, so I couldn't bring up that screen myself. When you tap 현금으로 바꾸기 (Convert to cash), you're first told to register your conversion details (전환 정보). The guide says Crack takes some time to verify the registered details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wltwj89dip50qb6z2z1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3wltwj89dip50qb6z2z1.webp" alt="The prompt shown after tapping 현금으로 바꾸기 (Convert to cash): conversion details only need to be registered once." width="800" height="573"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash-out starts at KRW 10,000 and pays on the 10th and 25th of each month
&lt;/h2&gt;

&lt;p&gt;Crack announced a revamp of its payout policy on 2026-07-29. &lt;strong&gt;From September 1, cash can be converted to money.&lt;/strong&gt; The name also changed from Wrtn cash to Crack cash. Exchanging cash for goods in the old cash store ended that day.&lt;/p&gt;

&lt;p&gt;The conversion terms are in the Crack cash guide (revision of 2026-09-03):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The minimum request is KRW 10,000, with no cap per request.&lt;/li&gt;
&lt;li&gt;There is no conversion fee.&lt;/li&gt;
&lt;li&gt;Payouts go out on the 10th and 25th of each month (the next business day if that date is a holiday), and you must apply at least 5 business days before a payout date.&lt;/li&gt;
&lt;li&gt;You register your name, resident registration number and a bank account in your own name, and submit copies of your ID and bankbook.&lt;/li&gt;
&lt;li&gt;Minors aged 14 or older also submit a guardian consent form, a family relation certificate and the guardian's ID. Their limits are KRW 2,000,000 per request and KRW 10,000,000 per month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ir0op4b4uzyhndmxzvu.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5ir0op4b4uzyhndmxzvu.webp" alt="The Crack cash screen, with the balance shown in won and the conversion-details and Convert to cash buttons." width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The balance is shown in won, not in cash units. In its Super Week notice, Crack said one Cracker is worth KRW 1. But I found no sentence stating that 1 cash equals KRW 1. The amounts in this post assume 1 cash = KRW 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversions of KRW 125,000 or less are not taxed under the statute
&lt;/h2&gt;

&lt;p&gt;Under Income Tax Act Art. 84, no tax is imposed when the other-income amount per payment is KRW 50,000 or less. Working backward with the 60% expense deduction, the boundary is a KRW 125,000 conversion. &lt;strong&gt;Up to KRW 125,000 the tax is KRW 0; at KRW 200,000, KRW 17,600 is deducted.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, Crack's documents don't say whether it treats one conversion as one payment under that article. The web code has no branch by amount either; it applies the same rate. The guide only says tax "may be" deducted.&lt;/p&gt;

&lt;p&gt;I don't recommend splitting conversions into small pieces to stay under the boundary. Without knowing how Crack counts payments, all you'd get is more requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cash saved over several months is reported in one lump on the payment date
&lt;/h2&gt;

&lt;p&gt;Withholding happens on the day money is actually paid out. If you convert several months of cash at once, it is not split by the month it accrued. The guide says it is reported in a lump sum based on the payout date. It also tells you to keep that in mind when filing your comprehensive income tax return.&lt;/p&gt;

&lt;p&gt;So on the payment statement Crack files with the National Tax Service, the amount is grouped under a single payout date. &lt;strong&gt;The figures to check in May are your conversion history, not your accrual history.&lt;/strong&gt; If you note the amount and the deduction for every conversion, reconciling later is easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crack's 8.8% and May income tax: at KRW 3,000,000 of other-income amount or less, you can choose separate taxation
&lt;/h2&gt;

&lt;p&gt;Korea's comprehensive income tax return (종합소득세) for the previous year is filed in May. As long as Crack cash is treated as other income, which path you take depends on your total other-income amount for the year. &lt;strong&gt;If your other-income amount is KRW 3,000,000 or less, you can choose separate taxation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That total also includes other withheld other income, such as lecture fees. Other income that the statute always taxes separately or always combines is left out of it (Income Tax Act Art. 14(3)8(a)).&lt;/p&gt;

&lt;p&gt;If Crack is your only such income and the 60% expense deduction applies, the boundary is KRW 7,500,000 of conversions. If your actual expenses exceed 60% and you deduct those instead, the boundary goes up (proviso to Enforcement Decree Art. 87(1-2)).&lt;/p&gt;

&lt;p&gt;If you choose separate taxation (분리과세), the withholding is final. If you have no other comprehensive income, you don't need to file in May (Art. 73(1)8). In return, you don't get back the tax already withheld.&lt;/p&gt;

&lt;p&gt;If you choose to combine (합산), it is added to your other income and recalculated at the basic progressive rates. I ran the numbers for someone with no income besides Crack. I included only the KRW 1,500,000 basic personal deduction and the KRW 70,000 standard tax credit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Annual conversions (KRW)&lt;/th&gt;
&lt;th&gt;Other-income amount (KRW)&lt;/th&gt;
&lt;th&gt;National tax withheld (KRW)&lt;/th&gt;
&lt;th&gt;Separate taxation&lt;/th&gt;
&lt;th&gt;Tax determined if combined (KRW)&lt;/th&gt;
&lt;th&gt;National tax refunded (KRW)&lt;/th&gt;
&lt;th&gt;Refund incl. local tax (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1,000,000&lt;/td&gt;
&lt;td&gt;400,000&lt;/td&gt;
&lt;td&gt;80,000&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;80,000&lt;/td&gt;
&lt;td&gt;88,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7,500,000&lt;/td&gt;
&lt;td&gt;3,000,000&lt;/td&gt;
&lt;td&gt;600,000&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;580,000&lt;/td&gt;
&lt;td&gt;638,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20,000,000&lt;/td&gt;
&lt;td&gt;8,000,000&lt;/td&gt;
&lt;td&gt;1,600,000&lt;/td&gt;
&lt;td&gt;Not available, must combine&lt;/td&gt;
&lt;td&gt;320,000&lt;/td&gt;
&lt;td&gt;1,280,000&lt;/td&gt;
&lt;td&gt;1,408,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50,000,000&lt;/td&gt;
&lt;td&gt;20,000,000&lt;/td&gt;
&lt;td&gt;4,000,000&lt;/td&gt;
&lt;td&gt;Not available, must combine&lt;/td&gt;
&lt;td&gt;1,445,000&lt;/td&gt;
&lt;td&gt;2,555,000&lt;/td&gt;
&lt;td&gt;2,810,500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once that total exceeds KRW 3,000,000, there is no choice. Combining is mandatory and you file by May 31. Even someone who converted KRW 50,000,000 in a year gets a refund if Crack is their only income. Withholding is 20% of the income amount. The tax on a KRW 18,500,000 tax base at the basic rates comes out lower than that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you have a salary, combining costs you more from the 24% marginal bracket up
&lt;/h2&gt;

&lt;p&gt;If you have employment income, the other-income amount sits on top of it. Compare the 20% withholding with your marginal rate. I calculated for an employee who converted KRW 10,000,000 in a year, which is an other-income amount of KRW 4,000,000:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Existing tax base (KRW)&lt;/th&gt;
&lt;th&gt;Marginal rate&lt;/th&gt;
&lt;th&gt;National tax difference if combined (KRW)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;Refund 560,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30,000,000&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Refund 200,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;70,000,000&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;td&gt;Additional payment 160,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;120,000,000&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;td&gt;Additional payment 600,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the added amount stays inside the 6% or 15% bracket, combining works in your favor. &lt;strong&gt;From the 24% bracket up, combining means paying more.&lt;/strong&gt; If the added amount crosses a bracket boundary, compare the tax with and without combining. When combining costs more and that eligible total is KRW 3,000,000 or less, ending it with separate taxation is the better choice.&lt;/p&gt;

&lt;p&gt;The KRW 4,000,000 in this example is over KRW 3,000,000, so combining is mandatory. This is a simple comparison that leaves out any change in deductions on the employment side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creators paid out every month should check the business registration test
&lt;/h2&gt;

&lt;p&gt;Right now Crack handles cash conversions as other income. But the platform doesn't decide the income category. Income Tax Act Art. 19(1)21 does. &lt;strong&gt;Income from activity carried on continuously and repeatedly for profit is business income.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Korea's National Tax Service published a tax guide for YouTubers and other new types of business, &lt;a href="https://www.nts.go.kr/comm/nttFileDownload.do?fileKey=56d32ae3eff35468305a07d1085cb387" rel="noopener noreferrer"&gt;유튜버 등 신종업종 사업자 세무안내&lt;/a&gt; (PDF, Korean). It says that even without a matching industry code, doing the activity continuously and repeatedly creates an obligation to register as a business. The test is scale and frequency, not whether you have registered. A creator who gets paid out every month may well meet it.&lt;/p&gt;

&lt;p&gt;If it becomes business income, a few things change:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Necessary expenses are no longer a fixed 60%; they come from your books or the official expense rates.&lt;/li&gt;
&lt;li&gt;If you create alone, it may qualify as VAT-exempt personal services. The basis is VAT Act Art. 26(1)15 and Enforcement Decree Art. 42.&lt;/li&gt;
&lt;li&gt;Under the National Health Insurance dependent rules, business income is counted separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Health insurance dependents: KRW 20,000,000 a year; dependent tax deduction: KRW 1,000,000 of income amount
&lt;/h3&gt;

&lt;p&gt;If you're listed as a dependent on a family member's National Health Insurance (피부양자) or claimed as a dependent on their tax return (부양가족), this part may matter more. &lt;strong&gt;To stay a health insurance dependent, your total income must be KRW 20,000,000 a year or less.&lt;/strong&gt; Business income is looked at separately. With no business registration and KRW 5,000,000 a year or less, it is treated as zero.&lt;/p&gt;

&lt;p&gt;I couldn't confirm whether other income is counted as gross receipts or as income amount here. If it's counted as income amount, the boundary is KRW 50,000,000 of conversions.&lt;/p&gt;

&lt;p&gt;The basic deduction for a dependent requires a total income amount of KRW 1,000,000 or less. With Crack income only, that is KRW 2,500,000 of conversions. Whether amounts taxed separately count toward it is left as unverified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the withholding record for Crack payouts on Hometax
&lt;/h2&gt;

&lt;p&gt;Hometax (홈택스) is the National Tax Service's e-filing site. Type 원천징수영수증 (withholding receipt) into its integrated search (통합검색) and 14 menus come up. &lt;strong&gt;Only one of them is for the person receiving income:&lt;/strong&gt; 지급명세서(원천징수 내역)등 조회 (payment statement / withholding history lookup), under 나의 소득·연말정산 (my income and year-end settlement). The rest are for companies making payments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1desagx13t9el6ffvsk.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp1desagx13t9el6ffvsk.webp" alt="Hometax integrated search results: the only menu for income earners is the payment statement lookup under 나의 소득·연말정산." width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The May return is filed under 세금신고 (tax filing) → 종합소득세 신고 (comprehensive income tax return). If you missed the deadline and only want your refund, there is a separate 종합소득세 기한후 환급신고 (late refund filing for comprehensive income tax) menu. I couldn't confirm when Crack payouts show up on the screen after you log in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you convert your cash
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a copy of your ID and a copy of a bankbook in your own name. Minors need three additional guardian documents.&lt;/li&gt;
&lt;li&gt;Verifying conversion details takes time, so register them well ahead of the 5-business-day cutoff before a payout date.&lt;/li&gt;
&lt;li&gt;Every time you convert, save the tax deduction and deposit amounts shown on the screen.&lt;/li&gt;
&lt;li&gt;Before May of the following year, check the Crack payouts in Hometax's payment statement lookup.&lt;/li&gt;
&lt;li&gt;If your eligible other-income amount for the year (Crack included, always-separate items excluded) is KRW 3,000,000 or less, choose whichever of separate taxation or combining works in your favor. If it's more, file a combined return.&lt;/li&gt;
&lt;li&gt;If payouts keep coming every month, first ask your tax office or a tax accountant whether it counts as business income.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;The 8.8% Crack deducts on cash-out is other-income withholding, not a fee, and if your other income is small, filing in May can get most of it back. Record every conversion, check the payout on Hometax before May, and if you're paid out every month, ask a tax office or accountant whether it is business income.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;p&gt;This post is not tax advice. I couldn't confirm the items below from original documents or screens, so they are stated with conditions. The conclusion can change depending on your facts.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cash-to-won conversion: there is no official statement. There is only a balance shown in won and a notice saying one Cracker is worth KRW 1.&lt;/li&gt;
&lt;li&gt;Which item of other income applies: Crack hasn't said. Whether it's Art. 21(1)15 or item 19, the deemed expense is 60% either way, so the 8.8% is the same.&lt;/li&gt;
&lt;li&gt;Whether the minimum taxable threshold applies per conversion: not confirmed. The web code applies the same rate regardless of the amount.&lt;/li&gt;
&lt;li&gt;Whether it counts as business income: this is a question of fact. I found no industry code or official ruling for AI character creation.&lt;/li&gt;
&lt;li&gt;Whether it qualifies as VAT-exempt personal services, and whether the penalty for not registering applies: not confirmed.&lt;/li&gt;
&lt;li&gt;Expense rates for tax year 2026: they are published in April 2027. So I did not calculate the tax for the business-income case.&lt;/li&gt;
&lt;li&gt;How other income is counted for health insurance dependents: I couldn't confirm whether the basis is gross receipts or income amount.&lt;/li&gt;
&lt;li&gt;Whether separately taxed amounts count toward the dependent tax deduction: I couldn't settle it from the statute alone.&lt;/li&gt;
&lt;li&gt;When payouts appear on Hometax: unknown, since I didn't see the screen after logging in.&lt;/li&gt;
&lt;li&gt;The actual rate on the conversion screen: I couldn't bring the screen up because my account balance is 0. I don't know whether the server's &lt;code&gt;taxRate&lt;/code&gt; is 0.088.&lt;/li&gt;
&lt;li&gt;How long the first payout takes to arrive: I haven't seen a case from the September payouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-30 First English version, adapted from the Korean original (Crack announcement and cash guide text, Income Tax Act articles cross-checked, conversion math reproduced, Crack cash screen checked)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cover image: AI-generated (OpenAI Images)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tax</category>
      <category>korea</category>
      <category>creators</category>
      <category>ai</category>
    </item>
    <item>
      <title>Parsing VRM in Flutter without a package: vrm_dart fails on VRM 1.0 (LateInitializationError), so read the GLB chunks yourself</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:20:59 +0000</pubDate>
      <link>https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l</link>
      <guid>https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l</guid>
      <description>&lt;p&gt;You do not need a package to read VRM metadata in Flutter. A VRM file is a glTF 2.0 GLB: a 12-byte header, then a JSON chunk carrying the model name, author, license, expressions, mesh list and the index of the thumbnail, then a BIN chunk carrying the thumbnail bytes. The one Dart library that exists, gatari/vrm_dart, throws &lt;code&gt;LateInitializationError&lt;/code&gt; on every VRM 1.0 file and, on VRM 0.x, returns a null thumbnail unless the image happens to be named "Thumbnail". The parser I ended up with is 101 lines of &lt;code&gt;dart:convert&lt;/code&gt; and &lt;code&gt;dart:typed_data&lt;/code&gt; and reads all of it on both versions.&lt;/p&gt;

&lt;p&gt;This is a full rewrite of a post I wrote in December 2024. It is for anyone who needs to show a VRM model's name, author and thumbnail inside a Flutter app. On 2026-09-22, on macOS Apple Silicon with Flutter 3.47.2 and Dart 3.13.2, I ran vrm_dart and a hand-written parser against five sample files, then dropped the parser into a Flutter package and ran &lt;code&gt;flutter test&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Feed a VRM 1.0 file to vrm_dart exactly the way the 2024 post did. &lt;code&gt;parse()&lt;/code&gt; returns, and the first access to the metadata throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== Seed-san.vrm (10917800 B)
parse(): ok
vrmMeta access: LateError: LateInitializationError: Field 'vrmMeta' has not been initialized.
thumbnailByteData: null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 2024 situation was this. The project had a Unity viewport and a Flutter UI, and the model list screen lived on the Flutter side, so the Flutter side had to read VRM metadata on its own. Unity has UniVRM and Unreal has VRM4U; Flutter had nothing. What I found was gatari/vrm_dart, a repository never published to pub. I copied two files into the project, and these three lines got me the thumbnail and part of the metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vrmByteData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vrmParser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VrmFileParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ByteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vrmByteData&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="n"&gt;vrmParser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post ended with the regret that BlendShapeGroup and the mesh list were out of reach. Since then the ground has moved. VRoid Studio exports VRM 1.0 from v1.20.0, released 2023-03 (checked 2026-09-22), so 1.0 files are now common. vrm_dart has had no commits since 2021-10-13, and a pub.dev search for vrm still returns zero VRM packages.&lt;/p&gt;

&lt;p&gt;VRM 0.x files trip it up in two more ways. A file with no images at all, like the add-on test fixture &lt;code&gt;minimal.vrm&lt;/code&gt;, dies inside &lt;code&gt;parse()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== minimal.vrm (4660 B)
parse(): _TypeError: type 'Null' is not a subtype of type 'List&amp;lt;dynamic&amp;gt;' in type cast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a 0.x file that does have a thumbnail still comes back with &lt;code&gt;thumbnailByteData&lt;/code&gt; null when the image is not named "Thumbnail", even though the metadata's &lt;code&gt;texture&lt;/code&gt; field points straight at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;vrm_dart only ever looks at the extension named &lt;code&gt;VRM&lt;/code&gt;. VRM 1.0 stores everything under &lt;code&gt;VRMC_vrm&lt;/code&gt;, with a different metadata schema and different names for the main fields: &lt;code&gt;title&lt;/code&gt; became &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt; became &lt;code&gt;authors&lt;/code&gt;, &lt;code&gt;licenseName&lt;/code&gt; became &lt;code&gt;licenseUrl&lt;/code&gt;, and the thumbnail moved from &lt;code&gt;texture&lt;/code&gt; to &lt;code&gt;thumbnailImage&lt;/code&gt;. With no branch for that, the &lt;code&gt;late&lt;/code&gt; field &lt;code&gt;vrmMeta&lt;/code&gt; is never assigned, and the first read throws.&lt;/p&gt;

&lt;p&gt;Start with the file structure. A VRM is a GLB, the binary container for glTF 2.0. The first 12 bytes are the header: the magic &lt;code&gt;glTF&lt;/code&gt;, version 2 and the total length, each a little-endian uint32. Chunks follow. Each chunk is a length and a type, 4 bytes each, then the payload. The standard chunk types are &lt;code&gt;JSON&lt;/code&gt; and &lt;code&gt;BIN&lt;/code&gt;; unknown types are skipped. All five sample files had chunk lengths that were multiples of 4. &lt;code&gt;minimal.vrm&lt;/code&gt; has a single JSON chunk and no BIN chunk at all.&lt;/p&gt;

&lt;p&gt;The VRM data lives under &lt;code&gt;extensions&lt;/code&gt; in the JSON chunk, and that is where 0.x and 1.0 diverge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;VRM 0.x&lt;/th&gt;
&lt;th&gt;VRM 1.0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Extension name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VRM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VRMC_vrm&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.title&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.name&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Author&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.author&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.authors&lt;/code&gt; (array)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.licenseName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;meta.licenseUrl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thumbnail&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.texture&lt;/code&gt; = index into &lt;code&gt;textures[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meta.thumbnailImage&lt;/code&gt; = index into &lt;code&gt;images[]&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expressions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;blendShapeMaster.blendShapeGroups[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expressions.preset&lt;/code&gt; / &lt;code&gt;expressions.custom&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meshes&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meshes[]&lt;/code&gt; (plain glTF)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;meshes[]&lt;/code&gt; (plain glTF)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The thumbnail path is the sharpest difference. The 0.x schema defines &lt;code&gt;texture&lt;/code&gt; as "Thumbnail of VRM model", and because it is a texture index you go through &lt;code&gt;textures[i].source&lt;/code&gt; before you reach &lt;code&gt;images[]&lt;/code&gt;. The 1.0 schema defines &lt;code&gt;thumbnailImage&lt;/code&gt; as "The index to the thumbnail image of the model in gltf.images" (checked 2026-09-22). In 1.0, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt; and &lt;code&gt;licenseUrl&lt;/code&gt; are required.&lt;/p&gt;

&lt;p&gt;vrm_dart takes neither path. It searches the &lt;code&gt;images&lt;/code&gt; array for an entry named "Thumbnail", so any other name is a miss, and it never reads &lt;code&gt;thumbnailImage&lt;/code&gt; at all. The third problem is casting: the generated code casts &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;bufferViews&lt;/code&gt; to non-null lists, which is where &lt;code&gt;_TypeError&lt;/code&gt; comes from on a file that has neither key.&lt;/p&gt;

&lt;p&gt;I compared the two parsers on five samples. For 0.x, the CC0 add-on test fixtures &lt;code&gt;minimal.vrm&lt;/code&gt; and &lt;code&gt;triangle.vrm&lt;/code&gt;. There is no public 0.x sample with a thumbnail, so I built two with Blender 5.2.2 LTS and VRM add-on 4.7.1, attaching a 64×64 PNG to &lt;code&gt;triangle.vrm&lt;/code&gt;: one with the image named "Thumbnail", one named "swas_thumb". For 1.0, VirtualCast's &lt;code&gt;Seed-san.vrm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65xm13s7cwb2rabyhwqp.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65xm13s7cwb2rabyhwqp.webp" alt="vrm_dart versus the direct parser across five VRM samples. vrm_dart reads everything only on the 0.x file whose image is named Thumbnail"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sample&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;vrm_dart meta&lt;/th&gt;
&lt;th&gt;vrm_dart thumbnail&lt;/th&gt;
&lt;th&gt;Direct parser&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;minimal.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;meta, 17 expressions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_TypeError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;meta, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle-thumb-Thumbnail.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;317 B&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;triangle-thumb-swas_thumb.vrm&lt;/td&gt;
&lt;td&gt;0.x&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 17 expressions, 1 mesh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seed-san.vrm&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LateInitializationError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;null&lt;/td&gt;
&lt;td&gt;meta, thumbnail, 18 expressions, 5 meshes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;vrm_dart reads exactly one row completely: the 0.x file whose image is named "Thumbnail". That is presumably the kind of file the 2024 post happened to succeed on. The direct parser reads all five. Seed-san's thumbnail came out as a 512×512 PNG of 192,765 bytes, and its meshes are hair, hair_tail, head, robo_arm and wear.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai4ynhwg737oz7wb8xcd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai4ynhwg737oz7wb8xcd.webp" alt="Console output from reading Seed-san.vrm with the direct parser: VRMC_vrm metadata, thumbnail, 18 expressions and 5 meshes"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Expression names changed between versions too. The 1.0 spec's expressions document lists the renames (checked 2026-09-22), and the 17 names in the 0.x samples and 18 in the 1.0 sample match this table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;VRM 0.x&lt;/th&gt;
&lt;th&gt;VRM 1.0&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;joy&lt;/td&gt;
&lt;td&gt;happy&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sorrow&lt;/td&gt;
&lt;td&gt;sad&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fun&lt;/td&gt;
&lt;td&gt;relaxed&lt;/td&gt;
&lt;td&gt;renamed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a, i, u, e, o&lt;/td&gt;
&lt;td&gt;aa, ih, ou, ee, oh&lt;/td&gt;
&lt;td&gt;lip sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;blink_l, blink_r&lt;/td&gt;
&lt;td&gt;blinkLeft, blinkRight&lt;/td&gt;
&lt;td&gt;spelling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lookup, lookdown, lookleft, lookright&lt;/td&gt;
&lt;td&gt;lookUp, lookDown, lookLeft, lookRight&lt;/td&gt;
&lt;td&gt;spelling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;surprised&lt;/td&gt;
&lt;td&gt;new in 1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;ol&gt;
&lt;li&gt;Read the GLB header and chunks. &lt;code&gt;ByteData&lt;/code&gt; from &lt;code&gt;dart:typed_data&lt;/code&gt; is enough, and the JSON chunk goes through &lt;code&gt;utf8.decode&lt;/code&gt; then &lt;code&gt;jsonDecode&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ByteData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mh"&gt;0x46546C67&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'not a GLB'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Endian&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;little&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x4E4F534A&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 'JSON'&lt;/span&gt;
     &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0x004E4942&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 'BIN'&lt;/span&gt;
     &lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;off&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'no JSON chunk'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Tell the versions apart from &lt;code&gt;extensions&lt;/code&gt;. &lt;code&gt;VRMC_vrm&lt;/code&gt; present means 1.0, &lt;code&gt;VRM&lt;/code&gt; means 0.x, and neither means it is a plain glTF, not a VRM:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'extensions'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;{};&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;is10&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'VRMC_vrm'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;is10&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;containsKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'VRM'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;FormatException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'glTF but neither VRM (0.x) nor VRMC_vrm (1.0) extension'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is10&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'VRMC_vrm'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ext&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'VRM'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'meta'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Read the metadata with the version's field names: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt; and &lt;code&gt;licenseName&lt;/code&gt; for 0.x; &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt; and &lt;code&gt;licenseUrl&lt;/code&gt; for 1.0. If a 1.0 file's &lt;code&gt;creditNotation&lt;/code&gt; is &lt;code&gt;required&lt;/code&gt;, the specification obliges whoever uses the model to display its credit. It does not say where or in what form, so that part is your app's decision.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resolve the thumbnail along the two separate paths. The image bytes are a slice of the BIN chunk given by &lt;code&gt;byteOffset&lt;/code&gt; and &lt;code&gt;byteLength&lt;/code&gt; of &lt;code&gt;bufferViews[i]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;   &lt;span class="n"&gt;Uint8List&lt;/span&gt; &lt;span class="nf"&gt;bufferView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;bv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferViews'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
     &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'byteOffset'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Uint8List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sublistView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'byteLength'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'thumbnailImage'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'textures'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'texture'&lt;/span&gt;&lt;span class="p"&gt;]][&lt;/span&gt;&lt;span class="s"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;imageIndex&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'images'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
   &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;thumbnail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferView'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bufferView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'bufferView'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Walk expressions and meshes with the right keys. On 0.x it is &lt;code&gt;presetName&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt; inside &lt;code&gt;blendShapeMaster.blendShapeGroups&lt;/code&gt;; on 1.0 it is the keys of &lt;code&gt;expressions.preset&lt;/code&gt; and &lt;code&gt;expressions.custom&lt;/code&gt;. Meshes are plain glTF: &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;primitives.length&lt;/code&gt; from &lt;code&gt;meshes[]&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep &lt;code&gt;dart:io&lt;/code&gt; out of the parser when it goes into a Flutter package. Take a single &lt;code&gt;Uint8List&lt;/code&gt;, so a file path, &lt;code&gt;rootBundle&lt;/code&gt; and &lt;code&gt;file_picker&lt;/code&gt; bytes all go through the same function. I put it into a &lt;code&gt;flutter create --template=package&lt;/code&gt; project this way; all 4 tests passed and the dependency count added is zero.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Treat image-less files as normal. &lt;code&gt;images&lt;/code&gt; and &lt;code&gt;bufferViews&lt;/code&gt; may be absent, and the BIN chunk may be absent too. The code above lets all of that fall through as &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Blender side of this is written up separately in &lt;a href="https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74"&gt;Blender 5.x and the VRM add-on: 5.2 LTS needs 4.4.0+, 5.3 alpha blocks even 4.7.1&lt;/a&gt;. The 0.x thumbnail samples in this post were built with that post's setup.&lt;/p&gt;

&lt;p&gt;Images: both are the measurement logs (&lt;code&gt;evidence/parse-glb.log&lt;/code&gt; and &lt;code&gt;evidence/vrm-dart-legacy.log&lt;/code&gt;) redrawn as a table and a console view. The 1.0 sample is Seed-san by VirtualCast, Inc., under the VRM Public License 1.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Skip the package search: the VRM metadata you want is one JSON chunk behind a 12-byte header, and the only real work is branching on &lt;code&gt;VRM&lt;/code&gt; versus &lt;code&gt;VRMC_vrm&lt;/code&gt;. If you must keep vrm_dart, know that it only works on 0.x files whose thumbnail image is literally named "Thumbnail".&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether 0.x files exported by VRoid Studio or UniVRM actually name the thumbnail image "Thumbnail": the file the 2024 post read no longer exists, and this post's 0.x thumbnail samples were made with the Blender add-on.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;dart:io&lt;/code&gt;-free parser on mobile and web Flutter, fed through &lt;code&gt;rootBundle&lt;/code&gt; or &lt;code&gt;file_picker&lt;/code&gt;: I only ran &lt;code&gt;flutter test&lt;/code&gt; on the host VM.&lt;/li&gt;
&lt;li&gt;Malformed files whose JSON chunk is not padded to 4 bytes: all five samples were multiples of 4. The code rounds the next chunk offset up to a multiple of 4, but I did not test such a file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2024-12-03 original Korean post (partial VRM 0.x meta and thumbnail via vrm_dart)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;2026-09-22 full rewrite (vrm_dart reproduced on Dart 3.13.2, zero-dependency direct parser measured on 5 samples across 0.x and 1.0, 4 flutter tests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.2&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>vrm</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Blender 5.x and the VRM add-on: 5.2 LTS needs 4.4.0+, 5.3 alpha blocks even 4.7.1 (blender_version_max is exclusive)</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Tue, 22 Sep 2026 06:45:52 +0000</pubDate>
      <link>https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74</link>
      <guid>https://dev.to/ilan_kim/blender-5x-and-the-vrm-add-on-52-lts-needs-440-53-alpha-blocks-even-471-4l74</guid>
      <description>&lt;p&gt;On Blender 5.2 LTS, VRM Addon for Blender only enables from 4.4.0 up, and on 5.3 alpha even the latest 4.7.1 refuses to enable. The cause is &lt;code&gt;blender_version_max&lt;/code&gt; in the extension manifest, which is an exclusive bound: it names the first Blender version the extension does &lt;strong&gt;not&lt;/strong&gt; support, not the last one it does. For 4.3.0 and newer, every combination the manifest gate blocks still enables through the legacy add-on zip, which has no version gate at all. The 3.9.0 line is the exception: it fails on both paths, because Blender 5.0 removed an API it imports.&lt;/p&gt;

&lt;p&gt;This is for anyone who upgraded to Blender 5.x and found the VRM add-on missing from the panel or refusing to switch on. On 2026-09-22, on macOS Apple Silicon, I installed four add-on versions into four Blender versions two different ways and ran all 32 cells headless. The Blender builds were 5.0.1, 5.1.2, 5.2.2 LTS and 5.3.0 alpha; the add-on versions were 3.9.0, 4.3.0, 4.4.0 and 4.7.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Drop the 4.3.0 extension zip into Blender 5.2.2 LTS. The install reports success, and then the add-on never turns on. The console has the reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR vrm: This Blender version (5.2.2) must be less than the maximum version (5.2.0)
Package was expected to be installed but not found: vrm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 5.3.0 alpha the latest 4.7.1 hits the same wall, with only the version in parentheses changing to &lt;code&gt;(5.3.0)&lt;/code&gt;. Inside the app it is quieter than that: the Extensions panel shows no error, because the VRM entry is not listed at all.&lt;/p&gt;

&lt;p&gt;Since Blender 4.2, add-ons ship as extensions, and an extension zip carries a &lt;code&gt;blender_manifest.toml&lt;/code&gt; at its root declaring &lt;code&gt;blender_version_min&lt;/code&gt; and &lt;code&gt;blender_version_max&lt;/code&gt;. When those values do not match the running Blender, the files unpack fine and activation is refused.&lt;/p&gt;

&lt;p&gt;VRM Addon for Blender publishes two zips per release:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension: &lt;code&gt;VRM_Addon_for_Blender-Extension-4_7_1.zip&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Legacy add-on: &lt;code&gt;VRM_Addon_for_Blender-4_7_1.zip&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two behave differently on 5.x, and that difference is the point of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;blender_version_max&lt;/code&gt; is not the last supported version. It is the first unsupported one. The manifest example in the Blender manual says so outright (checked 2026-09-22):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Optional: Blender version that the extension does not support, earlier versions are supported.&lt;/span&gt;
&lt;span class="py"&gt;blender_version_max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"5.1.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wording of the runtime error, &lt;code&gt;must be less than&lt;/code&gt;, means the same thing.&lt;/p&gt;

&lt;p&gt;So 4.3.0 declaring &lt;code&gt;blender_version_max = "5.2.0"&lt;/code&gt; is a statement that it supports up to 5.1.x. From 5.2.0 onward it is blocked whatever the patch number. The latest 4.7.1 declares &lt;code&gt;5.3.0&lt;/code&gt;, which covers 5.2 LTS and stops short of 5.3 alpha.&lt;/p&gt;

&lt;p&gt;I downloaded the manifest from all 71 release tags to find the boundaries. &lt;code&gt;blender_version_min&lt;/code&gt; is &lt;code&gt;4.2.0&lt;/code&gt; on every tag. Only &lt;code&gt;blender_version_max&lt;/code&gt; ever moved, and it moved four times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;blender_version_max&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Add-on range&lt;/th&gt;
&lt;th&gt;First tag&lt;/th&gt;
&lt;th&gt;Blender it enables on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5.0.0&lt;/td&gt;
&lt;td&gt;v3.9.0 – v3.15.0&lt;/td&gt;
&lt;td&gt;2025-07-19&lt;/td&gt;
&lt;td&gt;4.2 – 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.1.0&lt;/td&gt;
&lt;td&gt;v3.16.0 – v3.21.1&lt;/td&gt;
&lt;td&gt;2025-11-08&lt;/td&gt;
&lt;td&gt;4.2 – 5.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.2.0&lt;/td&gt;
&lt;td&gt;v3.22.0 – v4.3.3&lt;/td&gt;
&lt;td&gt;2026-03-16&lt;/td&gt;
&lt;td&gt;4.2 – 5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.3.0&lt;/td&gt;
&lt;td&gt;v4.4.0 – v4.7.1&lt;/td&gt;
&lt;td&gt;2026-07-11&lt;/td&gt;
&lt;td&gt;4.2 – 5.2 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sampling only the x.y.0 tags gets the boundaries wrong. Looking at v4.0.0 alone hid the entire 5.1.0 group of 22 tags. The "5.2 LTS needs 4.4.0 or newer" conclusion comes out of this table.&lt;/p&gt;

&lt;p&gt;Then I checked what actually enables, across 32 cells. Each cell got a fresh &lt;code&gt;BLENDER_USER_RESOURCES&lt;/code&gt; directory so it could not inherit state from a previous cell or from my own Blender config. O means the add-on enabled &lt;strong&gt;and&lt;/strong&gt; the roundtrip passed, where the roundtrip is importing a VRM file, exporting it, and importing the export again, for both a VRM 0.x and a VRM 1.0 sample.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbb3qcp6jcx9eyuu6tye.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffbb3qcp6jcx9eyuu6tye.webp" alt="Compatibility matrix for extension installs versus legacy add-on installs. The same zip diverges on 5.2.2 and 5.3.0 alpha" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Extension install:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Add-on \ Blender&lt;/th&gt;
&lt;th&gt;5.0.1&lt;/th&gt;
&lt;th&gt;5.1.2&lt;/th&gt;
&lt;th&gt;5.2.2 LTS&lt;/th&gt;
&lt;th&gt;5.3.0 alpha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.9.0&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.3.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.4.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.7.1&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legacy add-on install:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Add-on \ Blender&lt;/th&gt;
&lt;th&gt;5.0.1&lt;/th&gt;
&lt;th&gt;5.1.2&lt;/th&gt;
&lt;th&gt;5.2.2 LTS&lt;/th&gt;
&lt;th&gt;5.3.0 alpha&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3.9.0&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.3.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.4.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.7.1&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The legacy path has no version gate. 4.3.0 enabled on 5.2.2 and on 5.3.0 alpha and passed the roundtrip in both. That means the direct cause of the extension being refused on the spot is the manifest gate. It does not mean those combinations are free of other API incompatibilities — this run only went as far as activation plus a two-file roundtrip.&lt;/p&gt;

&lt;p&gt;Of the four versions I tested, only the 3.9.0 line fails to activate at all. Install it the legacy way and it still raises during module load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportError: cannot import name 'ShaderNodeTexPointDensity' from 'bpy.types' (unknown location)
This add-on is not compatible with Blender version 5.0 or later. Your current version is 5.2.2.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blender 5.0 removed &lt;code&gt;bpy.types.ShaderNodeTexPointDensity&lt;/code&gt;, 3.9.0 imports that type at module load, and the add-on carries its own guard that prints the second line. There is no install path that makes this combination work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmlq1ng3uwnefb92xbtl0.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmlq1ng3uwnefb92xbtl0.webp" alt="Console output from putting the 4.3.0 extension zip into Blender 5.2.2 LTS, showing must be less than the maximum version (5.2.0)" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The silence in the Extensions panel has its own explanation. The panel queries the &lt;code&gt;extensions.blender.org&lt;/code&gt; API filtered by Blender version, and the filtering happens server side. Ask with &lt;code&gt;blender_version=5.2.2&lt;/code&gt; and you get vrm 4.7.1 back; ask with &lt;code&gt;5.3.0&lt;/code&gt; and there is no vrm entry in the response at all (checked 2026-09-22). The panel is not hiding an error, it is being handed an empty result.&lt;/p&gt;

&lt;p&gt;The roundtrip passed in all 20 cells that enabled. Two samples were used: &lt;code&gt;minimal.vrm&lt;/code&gt; for VRM 0.x, which is CC0, and VirtualCast's &lt;code&gt;Seed-san.vrm&lt;/code&gt; for VRM 1.0, licensed under the VRM Public License 1.0. Exporting Seed-san grows it from 10,917,800 bytes to somewhere between 16,549,208 and 16,551,476 bytes — the exact figure varies slightly per combination, and all of them land near 152% of the original. I did not investigate why it grows. Whether it is related to the texture duplication issue #1114 is a separate question I have not answered.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Check your Blender version first. On 5.2 LTS you need add-on 4.4.0 or newer. On 5.0 and 5.1 the latest 4.7.1 enables fine. If you are pinned to an older add-on, read the right-hand column of the boundary table above.&lt;/li&gt;
&lt;li&gt;If VRM is missing from the Extensions panel, grab the extension zip from the GitHub releases directly — &lt;code&gt;VRM_Addon_for_Blender-Extension-&amp;lt;version&amp;gt;.zip&lt;/code&gt; — and use Install from Disk. The panel only ever offers the single latest version.&lt;/li&gt;
&lt;li&gt;If the extension is refused with &lt;code&gt;must be less than the maximum version&lt;/code&gt;, the legacy zip is a way through. Install &lt;code&gt;VRM_Addon_for_Blender-&amp;lt;version&amp;gt;.zip&lt;/code&gt; from the same release as an add-on rather than an extension. It is also the only way to run 4.7.1 on 5.3 alpha. Be aware that this puts you outside the range the maintainer declares support for; if something breaks, moving the add-on to a version inside the supported range is the first thing to try.&lt;/li&gt;
&lt;li&gt;The top-level folder name inside the legacy zip changes between versions. 3.9.0 unpacks to &lt;code&gt;VRM_Addon_for_Blender-3_9_0&lt;/code&gt;, and the 4.x releases unpack to &lt;code&gt;VRM_Addon_for_Blender-release&lt;/code&gt;. If you are enabling it from a script, discover the module with &lt;code&gt;addon_utils.modules()&lt;/code&gt; rather than hardcoding the name.&lt;/li&gt;
&lt;li&gt;Give up on the 3.9.0 line under 5.x. &lt;code&gt;ShaderNodeTexPointDensity&lt;/code&gt; is gone and no install path gets around it.&lt;/li&gt;
&lt;li&gt;If you took the legacy route, export a VRM and import it back. That roundtrip is as far as this measurement went.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reading VRM files from a Flutter app rather than Blender is written up separately in &lt;a href="https://dev.to/ilan_kim/parsing-vrm-in-flutter-without-a-package-vrmdart-fails-on-vrm-10-lateinitializationerror-so-e8l"&gt;Parsing VRM in Flutter without a package&lt;/a&gt;. The VRM 0.x thumbnail samples in that post were built with the 5.2.2 LTS and 4.7.1 combination from this one.&lt;/p&gt;

&lt;p&gt;Images: both are the measurement logs (&lt;code&gt;evidence/matrix-install.log&lt;/code&gt;) redrawn as a table and a console view.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Read &lt;code&gt;blender_version_max&lt;/code&gt; as the first version that is excluded, and pick the add-on from the boundary table rather than from the panel. The legacy zip bypasses the gate, but bypassing a declared support range is a workaround, not a fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Whether combinations enabled via the legacy bypass are fully functional: only the roundtrip was checked. Spring bones and the expression editing panels were not.&lt;/li&gt;
&lt;li&gt;Windows and Linux behaviour: this ran on a single macOS Apple Silicon machine. The manifest gate is platform independent, but I could not confirm that 3.9.0 raises the same message elsewhere.&lt;/li&gt;
&lt;li&gt;When the add-on will support Blender 5.3 after its stable release: these numbers are from 5.3.0 alpha. If &lt;code&gt;blender_version_max&lt;/code&gt; moves to &lt;code&gt;5.4.0&lt;/code&gt;, the 5.3 column changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-22 first version (Blender 5.0.1 – 5.3.0 alpha x add-on 3.9.0 – 4.7.1, 32 cells across extension and legacy installs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;2026-09-22 added the cross-link to the Flutter VRM parsing post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blender</category>
      <category>python</category>
      <category>vrm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Calling the NovelAI V5 API directly: nai-diffusion-5-full request body, params_version 4/3/1/0 identical, missing v4_prompt is a 500</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Mon, 14 Sep 2026 01:09:22 +0000</pubDate>
      <link>https://dev.to/ilan_kim/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-paramsversion-4310-133a</link>
      <guid>https://dev.to/ilan_kim/calling-the-novelai-v5-api-directly-nai-diffusion-5-full-request-body-paramsversion-4310-133a</guid>
      <description>&lt;p&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt; takes the same body skeleton V4 took, posted to &lt;code&gt;https://image.novelai.net/ai/generate-image&lt;/code&gt;, and &lt;code&gt;params_version&lt;/code&gt; — the one field every third-party tool fills in differently — returns a pixel-for-pixel identical image whether you send 4, 3, 1, or 0 with this body skeleton. JSON and multipart bodies gave the same result too. What does break it: drop either &lt;code&gt;v4_prompt&lt;/code&gt; or &lt;code&gt;v4_negative_prompt&lt;/code&gt; and you get &lt;code&gt;500 Internal Server Error&lt;/code&gt; rather than a 400, set &lt;code&gt;Content-Type&lt;/code&gt; by hand on a &lt;code&gt;FormData&lt;/code&gt; body and you get a &lt;code&gt;missing multipart boundary&lt;/code&gt; 400, and post to the old host and you get a &lt;code&gt;model must be a valid enum value&lt;/code&gt; 400.&lt;/p&gt;

&lt;p&gt;This is for people calling NovelAI V5 from their own scripts rather than through a ComfyUI node or a GUI like NAIA (a community-built desktop front end for NovelAI). On 2026-09-14, on macOS 26.5.2 with Node v22.22.3 and an Opus account (&lt;code&gt;tier: 3&lt;/code&gt;), I generated six images inside the free band and compared request formats, error responses, and balance changes; I also read the strings out of the official web client bundle the same day. Two terms if NovelAI is new to you: Anlas is NovelAI's generation credit, and Opus is the top subscription tier, which generates one image at a time at the common sizes and up to 28 steps without spending any, as long as no base image is involved and the allowance has not run out. The policy and the per-size cost table are in &lt;a href="https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l"&gt;the earlier post&lt;/a&gt;, the measurements through a ComfyUI node are in &lt;a href="https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl"&gt;the one after it&lt;/a&gt;, and this post covers only the request contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;If you look at third-party tools' request bodies to work out how to call V5 yourself, the &lt;code&gt;params_version&lt;/code&gt; value is different in every one of them. ComfyUI_NAIDGenerator, a ComfyUI node for NovelAI, sends &lt;code&gt;1&lt;/code&gt;. The README of ComfyUI_RS_NAI_API_Request, another ComfyUI node, says "V5 requests use params_version: 4 while older models keep params_version: 3". The changelog of NAIWeaver, a standalone NovelAI client, says "send params_version: 4" (all checked 2026-09-14). The official OpenAPI document has no &lt;code&gt;required&lt;/code&gt; array and no &lt;code&gt;nai-diffusion-5&lt;/code&gt; string, so it cannot settle which one is right.&lt;/p&gt;

&lt;p&gt;From there, nudging the body around gets you three responses. All three are verbatim from this round of testing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://api.novelai.net/ai/generate-image  (old host)
400 {"statusCode":400,"message":"model must be a valid enum value"}

POST https://image.novelai.net/ai/generate-image  (v4_prompt or v4_negative_prompt omitted)
500 {"statusCode":500,"message":"Internal Server Error"}

POST https://image.novelai.net/ai/generate-image  (FormData + Content-Type: multipart/form-data set by hand)
400 {"statusCode":400,"message":"missing multipart boundary"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one has different wording from the "update to the image URL" 400 the balance lookup returns. The old host accepts the generation request itself but does not know the V5 model ID, so from the message alone it looks like you typed the model ID wrong.&lt;/p&gt;

&lt;p&gt;The second is the confusing one. It is a 500 rather than a 400-class validation error, so it reads like a server outage, but the cause is your request body. The third one appears when you hand Node's &lt;code&gt;fetch&lt;/code&gt; a &lt;code&gt;FormData&lt;/code&gt; body and set the header yourself, because the boundary &lt;code&gt;undici&lt;/code&gt; would have attached automatically disappears.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the measurements show
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;params_version&lt;/code&gt; does not change the result with this body skeleton. Same prompt, seed &lt;code&gt;1234567890&lt;/code&gt;, 832×1216, 28 steps, one image per value:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;params_version&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Response time&lt;/th&gt;
&lt;th&gt;PNG &lt;code&gt;Source&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Anlas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4 (official web client)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2.5 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,930 → 9,930&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 (the value said to be for older models)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 (ComfyUI_NAIDGenerator)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.9 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0 (a value no tool uses)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no change&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Comparing the four images pixel by pixel with Pillow gives a mean absolute difference of 0 and 0% of pixels changed, so they are completely identical. The &lt;code&gt;Comment&lt;/code&gt; metadata of the returned PNGs has no &lt;code&gt;params_version&lt;/code&gt; field either. If the value were read and acted on, 0 should at least have been rejected, so with this skeleton it has to be treated as ignored. Whether that also holds for other field combinations — character prompts, inpainting and so on — I did not check.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwdmardaaceuag9wtf94.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftwdmardaaceuag9wtf94.webp" alt="A contact sheet placing the four images received with params_version 4, 3, 1, and 0 side by side; all four are identical down to the pixel" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So where does &lt;code&gt;4&lt;/code&gt; come from? In the novelai.net bundle I pulled on 2026-09-14 (&lt;code&gt;_app-b7172cc1a6a0b340.js&lt;/code&gt;), the default parameters in the &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; branch start with &lt;code&gt;params_version:4&lt;/code&gt;, and there is migration code that raises stored settings to 4 if &lt;code&gt;params_version&amp;lt;4&lt;/code&gt;. The string &lt;code&gt;v5_prompt&lt;/code&gt; appears 0 times; &lt;code&gt;v4_prompt&lt;/code&gt; appears twice.&lt;/p&gt;

&lt;p&gt;The transport format makes no difference to the result either. Sending the &lt;code&gt;params_version: 1&lt;/code&gt; body as JSON and sending it as a JSON blob in the &lt;code&gt;request&lt;/code&gt; part of a &lt;code&gt;FormData&lt;/code&gt; both returned 200 with identical pixels. The debug toggle text in that same bundle describes the default as "Image generation requests will be sent as multipart form data." and the checked state as "JSON (pre-multipart-form format)", so multipart is the official client's default and JSON survives as the legacy format.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Content-Type&lt;/code&gt; handling&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;application/json&lt;/code&gt; set explicitly&lt;/td&gt;
&lt;td&gt;200, 917,294-byte zip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multipart&lt;/td&gt;
&lt;td&gt;header not set (automatic boundary)&lt;/td&gt;
&lt;td&gt;200, pixels identical to JSON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;multipart&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;multipart/form-data&lt;/code&gt; set by hand&lt;/td&gt;
&lt;td&gt;400 &lt;code&gt;missing multipart boundary&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You cannot leave out &lt;code&gt;v4_prompt&lt;/code&gt; or &lt;code&gt;v4_negative_prompt&lt;/code&gt;. With everything else left alone, a body with both keys removed, a body with only &lt;code&gt;v4_prompt&lt;/code&gt; removed, and a body with only &lt;code&gt;v4_negative_prompt&lt;/code&gt; removed all came back with the same 500, and none of them moved the balance. Together with the absence of &lt;code&gt;v5_prompt&lt;/code&gt; in the bundle, I take this to mean V5 still requires the V4 structure as it is.&lt;/p&gt;

&lt;p&gt;Authentication errors all arrive lumped into a single 401. Leave the &lt;code&gt;Authorization&lt;/code&gt; header out entirely, or put a space inside the token value, and the response is the same &lt;code&gt;401 {"statusCode":401,"message":"Unauthorized"}&lt;/code&gt; either way. Since the response cannot tell you whether the token string is mangled or the header is missing, don't print the value: check its length and whether it is unchanged by &lt;code&gt;trim()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the free band, this adds the landscape size to the table in the earlier post. 1216×832 (pixel product 1,011,712) at 28 steps, one image, was also 0 Anlas, and across six generations &lt;code&gt;usage.percent&lt;/code&gt; did not move from 87. The battery reading is an integer, which means per-image consumption below 1% is invisible in it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7zljdpfdowslshd6836.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe7zljdpfdowslshd6836.webp" alt="A V5 Full sample received at the 1216×832 landscape size; one image at 28 steps, so 0 Anlas" width="799" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I did not find an API that quotes the price up front this time. &lt;code&gt;POST /ai/generate-image/request-price&lt;/code&gt;, which appears in the web bundle, returned &lt;code&gt;404 page not found&lt;/code&gt; on &lt;code&gt;image.novelai.net&lt;/code&gt; and &lt;code&gt;404 Cannot POST /ai/generate-image/request-price&lt;/code&gt; on &lt;code&gt;api.novelai.net&lt;/code&gt;. The web UI does show an estimated Anlas cost on the generate button, but I could not get the same number out of this path, and whether another path exists I could not confirm.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Use one host, &lt;code&gt;https://image.novelai.net&lt;/code&gt;. Generation (&lt;code&gt;/ai/generate-image&lt;/code&gt;) and balance lookup (&lt;code&gt;/user/subscription&lt;/code&gt;) are both on it, while &lt;code&gt;api.novelai.net&lt;/code&gt; rejects generation with &lt;code&gt;model must be a valid enum value&lt;/code&gt; and lookups with "update to the image URL".&lt;/li&gt;
&lt;li&gt;Use the body below as it stands. This JSON is the full body that returned 200 in the &lt;code&gt;params_version: 4&lt;/code&gt; row of the table above, and the prompt goes in the same string in three places: top-level &lt;code&gt;input&lt;/code&gt;, &lt;code&gt;parameters.prompt&lt;/code&gt;, and &lt;code&gt;v4_prompt.caption.base_caption&lt;/code&gt;. Use &lt;code&gt;4&lt;/code&gt; for &lt;code&gt;params_version&lt;/code&gt; to match the official web client, but don't read any meaning into the value:
&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nai-diffusion-5-full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"generate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"params_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;832&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1216&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sampler"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"k_euler_ancestral"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"n_samples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ucPreset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qualityToggle"&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;"sm"&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;"sm_dyn"&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;"dynamic_thresholding"&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;"controlnet_strength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legacy"&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;"add_original_image"&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;"cfg_rescale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"noise_schedule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"karras"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legacy_v3_extend"&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;"uncond_scale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"negative_prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reference_image_multiple"&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;"reference_information_extracted_multiple"&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;"reference_strength_multiple"&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;"extra_noise_seed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"v4_prompt"&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;"use_coords"&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;"use_order"&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;"caption"&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;"base_caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1girl, solo, silver hair, school uniform, looking at viewer, upper body, simple background, masterpiece, best quality"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"char_captions"&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="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;"v4_negative_prompt"&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;"use_coords"&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;"use_order"&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;"caption"&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;"base_caption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"char_captions"&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="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="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;Save that JSON as &lt;code&gt;body.json&lt;/code&gt;, put the file below in the same folder, and it runs as is. The key file is a text file with a &lt;code&gt;pst-&lt;/code&gt; token on the line after a line labeled "novelai", and the token stays in memory only. Running it with &lt;code&gt;node nai-v5-direct.mjs &amp;lt;key file&amp;gt;&lt;/code&gt; got me a 200 and an &lt;code&gt;image_0.png&lt;/code&gt; whose pixels match the table above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// node nai-v5-direct.mjs &amp;lt;key file&amp;gt; — the key file has a pst- token on the line after the "novelai" label&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;writeFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs/promises&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\r?\n&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/novelai/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;body.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://image.novelai.net/ai/generate-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;out.zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt; &lt;span class="c1"&gt;// image_0.png is inside&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;For transport, set &lt;code&gt;Content-Type: application/json&lt;/code&gt; explicitly if you send JSON, and do not set a &lt;code&gt;Content-Type&lt;/code&gt; header yourself if you use &lt;code&gt;FormData&lt;/code&gt;. Node 22's &lt;code&gt;fetch&lt;/code&gt; builds the header with the boundary on its own for a &lt;code&gt;FormData&lt;/code&gt; body.&lt;/li&gt;
&lt;li&gt;The response is a &lt;code&gt;binary/octet-stream&lt;/code&gt; zip with &lt;code&gt;image_0.png&lt;/code&gt; inside. Check the model from the &lt;code&gt;Source&lt;/code&gt; value in the PNG's &lt;code&gt;tEXt&lt;/code&gt; chunk (&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;To stay at 0 Anlas, keep all of it: Opus, one image (&lt;code&gt;n_samples: 1&lt;/code&gt;), no base image, a pixel product of 1,048,576 or less, and 28 steps or fewer — then read &lt;code&gt;/user/subscription&lt;/code&gt; before and after the generation and compare the sum &lt;code&gt;trainingStepsLeft.fixedTrainingStepsLeft + trainingStepsLeft.purchasedTrainingSteps&lt;/code&gt;. &lt;code&gt;trainingStepsLeft&lt;/code&gt; is an object rather than an integer, so you cannot subtract it directly. &lt;code&gt;usage.percent&lt;/code&gt; is too coarse to use for per-image checks.&lt;/li&gt;
&lt;li&gt;Respect the API document's "all generation requests must be initiated by a human action" and its 429 note ("API clients should not attempt to retry") by sending human-initiated requests one at a time with a gap between them. This round of measurements ran with 6 seconds between requests and a concurrency of 1.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample outputs in this post are AI images generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;With this body skeleton, &lt;code&gt;params_version&lt;/code&gt; is ignored — 4, 3, 1, and 0 all return the same pixels — and JSON and multipart bodies produced the same image. The parts that actually matter are the host, &lt;code&gt;image.novelai.net&lt;/code&gt;, and keeping both &lt;code&gt;v4_prompt&lt;/code&gt; and &lt;code&gt;v4_negative_prompt&lt;/code&gt; in the body, since dropping either one returns a 500 rather than a validation 400.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How a 429 behaves: the API document explicitly forbids retrying, so I didn't provoke one on purpose. 429 isn't even documented for the image endpoints, so I don't know the actual message.&lt;/li&gt;
&lt;li&gt;What happens when &lt;code&gt;director_reference_*&lt;/code&gt; fields are sent to V5: V5 doesn't support Precise Reference, but the schema doesn't block these fields. Whether that is a 400 or "ignored and billed", I didn't run, because of the billing risk.&lt;/li&gt;
&lt;li&gt;The actual charge for a 29-step or 2-image request: that is the paid band, so I didn't generate it. The price lookup path I tried was a 404, so the only figure I have is the 45 Anlas for the large size from the earlier post.&lt;/li&gt;
&lt;li&gt;The absolute capacity of the Opus battery (allowance): &lt;code&gt;usage.percent&lt;/code&gt; stayed fixed at 87 across six images, so I couldn't work backwards to a per-image draw.&lt;/li&gt;
&lt;li&gt;How &lt;code&gt;params_version&lt;/code&gt; behaves with other body skeletons: I didn't run it with character-prompt, inpainting, or img2img bodies. Large values such as 9 also fell outside the generation budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-14 first version (macOS 26.5.2 · Node v22.22.3 · Opus, 6 free-tier generations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>api</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Running NovelAI V5 in ComfyUI: nai-diffusion-5-full works, and the Error fetching Anlas 400 is a moved endpoint</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Sun, 13 Sep 2026 07:59:16 +0000</pubDate>
      <link>https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl</link>
      <guid>https://dev.to/ilan_kim/running-novelai-v5-in-comfyui-nai-diffusion-5-full-works-and-the-error-fetching-anlas-400-is-a-18gl</guid>
      <description>&lt;p&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt; and &lt;code&gt;nai-diffusion-5-curated&lt;/code&gt; work as they are through the ComfyUI_NAIDGenerator node, even though the README says the IDs haven't been checked against the live API. The &lt;code&gt;Error fetching Anlas&lt;/code&gt; 400 that shows up next to them has nothing to do with the models: the node still asks &lt;code&gt;https://api.novelai.net/user/data&lt;/code&gt; for your balance, and NovelAI moved its user endpoints to &lt;code&gt;image.novelai.net&lt;/code&gt;, so two spots in &lt;code&gt;nodes.py&lt;/code&gt; fix it.&lt;/p&gt;

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

&lt;p&gt;The &lt;code&gt;Source&lt;/code&gt; metadata in the PNGs the node brought back read &lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt; and &lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt; respectively, and on an Opus account one image at Normal size (832×1216) and 28 steps cost 0 Anlas. Two terms if NovelAI is new to you: Anlas is NovelAI's generation credit, and Opus is the top subscription tier, which generates the common sizes without spending any.&lt;/p&gt;

&lt;p&gt;I wrote this for people who read the README line "have not been independently confirmed against the live API yet" and hesitated, and for anyone hitting &lt;code&gt;Error fetching Anlas&lt;/code&gt; the way issue #51 describes. I installed ComfyUI v0.35.0 fresh on macOS and pinned the node to its 2026-08-21 commit, &lt;code&gt;52da75a&lt;/code&gt; (package version 1.0.9).&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Wire the Anlas Tracker node into Preview Any and run it, and the preview text reads &lt;code&gt;Error fetching Anlas&lt;/code&gt; while the console prints one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[NovelAI] Failed to fetch Anlas balance: 400 Client Error: Bad Request for url: https://api.novelai.net/user/data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node added the two V5 model IDs in PR #50 on 2026-08-21, but the README still carries the caveat that they were never compared against the live API. Issue #51, opened the next day, reports a generation 400 on a trial account together with &lt;code&gt;Error fetching Anlas&lt;/code&gt;, and as of 2026-09-13 it has no replies.&lt;/p&gt;

&lt;p&gt;Meanwhile NovelAI moved its user endpoints to &lt;code&gt;image.novelai.net&lt;/code&gt; around the V5 launch. The 400 from the old host is exactly what I saw in &lt;a href="https://swiftlyaside.tistory.com/7" rel="noopener noreferrer"&gt;my earlier Korean post&lt;/a&gt;, so I checked whether the node was failing for the same reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;It is because &lt;code&gt;_get_user_data&lt;/code&gt; in &lt;code&gt;nodes.py&lt;/code&gt; calls &lt;code&gt;/user/data&lt;/code&gt; with &lt;code&gt;USER_API_BASE_URL = "https://api.novelai.net"&lt;/code&gt;. Attach a token and call both hosts directly, and their answers diverge:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Call&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET https://api.novelai.net/user/data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;400 &lt;code&gt;{"statusCode":400,"message":"Please refresh NovelAI.net. If using a third-party tool, update to the image URL."}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET https://image.novelai.net/user/data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200, includes &lt;code&gt;subscription.trainingStepsLeft&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without a token both hosts return 401, so reproducing this needs a valid one. Swapping the host isn't the end of it either. On the new host &lt;code&gt;trainingStepsLeft&lt;/code&gt; is not an integer but a shape like &lt;code&gt;{"fixedTrainingStepsLeft": 9898, "purchasedTrainingSteps": 32}&lt;/code&gt;, so the Tracker text prints the dictionary verbatim and the Generate node's before-and-after subtraction &lt;code&gt;start_anlas - final_anlas&lt;/code&gt; dies with &lt;code&gt;TypeError: unsupported operand type(s) for -: 'dict' and 'dict'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The model IDs themselves were fine. Here is what a ModelOption → Generate → Save Image workflow returned over &lt;code&gt;POST /prompt&lt;/code&gt;, with the same prompt, seed, and 28 steps as the earlier post:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;PNG &lt;code&gt;Source&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;Subscription Anlas&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216&lt;/td&gt;
&lt;td&gt;200, 4.1 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,898 → 9,898&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-curated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768&lt;/td&gt;
&lt;td&gt;200, 1.6 s&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9,898 → 9,898&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9va5zh9grilhxjypz8j5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9va5zh9grilhxjypz8j5.webp" alt="A V5 Full normal-size output received through the ComfyUI_NAIDGenerator node" width="800" height="1169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is one trap. The PNG that Save Image writes keeps only the &lt;code&gt;prompt&lt;/code&gt; chunk, and the &lt;code&gt;Source&lt;/code&gt; field NovelAI set is gone. The node saves the original separately under a running number such as &lt;code&gt;output/NAI_autosave_00001_.png&lt;/code&gt;, so read the metadata from the file whose number matches the run you just made.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb79v9mkwwtjr1ohccqak.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb79v9mkwwtjr1ohccqak.webp" alt="The ComfyUI graph after the patch: the Anlas Tracker node feeding Preview Any, which shows 9930 Anlas" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Fix &lt;code&gt;_get_user_data&lt;/code&gt; in &lt;code&gt;nodes.py&lt;/code&gt; in two places. Point the host at &lt;code&gt;image.novelai.net&lt;/code&gt;, and normalize the dictionary-shaped &lt;code&gt;trainingStepsLeft&lt;/code&gt; into an integer total:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-    USER_API_BASE_URL = "https://api.novelai.net"
&lt;/span&gt;&lt;span class="gi"&gt;+    USER_API_BASE_URL = "https://image.novelai.net"
&lt;/span&gt;@@
     response.raise_for_status()
&lt;span class="gd"&gt;-    return response.json()
&lt;/span&gt;&lt;span class="gi"&gt;+    data = response.json()
+    steps = data.get("subscription", {}).get("trainingStepsLeft")
+    if isinstance(steps, dict):
+        data["subscription"]["trainingStepsLeft"] = int(steps.get("fixedTrainingStepsLeft", 0) or 0) + int(steps.get("purchasedTrainingSteps", 0) or 0)
+    return data
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Restart the server and run the Anlas Tracker again, and it shows &lt;code&gt;9930 Anlas&lt;/code&gt;. That is the subscription 9,898 plus the purchased 32, the same number the web UI shows as your total, and when I generated one more V5 image at Normal size after the patch, the console printed &lt;code&gt;Generation cost: 0 Anlas&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For V5, picking &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; in ModelOption is all there is to it. On Opus, leave &lt;code&gt;limit_opus_free&lt;/code&gt; on and Normal size at 28 steps or fewer stays in the free band.&lt;/li&gt;
&lt;li&gt;I sent the same patch upstream as &lt;a href="https://github.com/bedovyy/ComfyUI_NAIDGenerator/pull/52" rel="noopener noreferrer"&gt;PR #52&lt;/a&gt;, along with a correction to the unconfirmed line in the README. I also posted the reproduction log on issue #51 with a link to this article.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample output was generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;Both V5 model IDs work through the node as shipped, and the &lt;code&gt;Error fetching Anlas&lt;/code&gt; 400 is the balance lookup still pointing at &lt;code&gt;api.novelai.net&lt;/code&gt;. Fixing it takes the new host plus one normalization, because &lt;code&gt;trainingStepsLeft&lt;/code&gt; now arrives as a dictionary rather than an integer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The generation 400 on a trial account: I don't have a trial account, so I couldn't reproduce the generation failure from issue #51. Whether it shares a cause with the balance lookup failure, I don't know.&lt;/li&gt;
&lt;li&gt;Regression on V4.5 and older models: I judged from the code alone that the patch touches only the balance lookup function and leaves the generation path alone, and I did not re-run a V4.5 generation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-13 first written (ComfyUI 0.35.0 + ComfyUI_NAIDGenerator 52da75a, measured on an Opus account)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>comfyui</category>
      <category>novelai</category>
      <category>python</category>
      <category>api</category>
    </item>
    <item>
      <title>NovelAI V5 on Opus: usage limits, the 2026-09-21 subscription Anlas reset, and the api.novelai.net to image.novelai.net move</title>
      <dc:creator>Ilan Kim</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:28:39 +0000</pubDate>
      <link>https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l</link>
      <guid>https://dev.to/ilan_kim/novelai-v5-on-opus-usage-limits-the-2026-09-21-subscription-anlas-reset-and-the-apinovelainet-567l</guid>
      <description>&lt;p&gt;On an Opus account (NovelAI's top subscription tier), NovelAI Diffusion V5 still costs 0 Anlas (NovelAI's generation credits) up to Normal size (832×1216) at 28 steps and one image per request; Large (1024×1536) is where 45 Anlas starts coming out of your balance. From September 21, 2026, your unused subscription Anlas resets to 0 when your subscription period ends, and the balance endpoint has moved from &lt;code&gt;api.novelai.net&lt;/code&gt; to &lt;code&gt;image.novelai.net&lt;/code&gt;. The reset is tied to the end of your period, not to the effective date: on September 22, the day after it took effect, this account's balance was still untouched.&lt;/p&gt;

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

&lt;p&gt;V5 shipped on 2026-08-21, and with it came a "battery" usage limit on Opus subscriptions. The announcement doesn't say how much a single generation draws or where you can see it, so I ran the numbers myself against the API. The reset itself is still a policy statement here: this account's paid period runs to September 29, so the day-after query only shows that nothing is wiped on the effective date. I measured on 2026-09-11 and 2026-09-13, and queried once more on 2026-09-22.&lt;/p&gt;

&lt;p&gt;I wrote this for two groups: people deciding whether to keep or cancel Opus, and people driving the NovelAI API directly from tools like ComfyUI nodes. In that order: the per-size cost table, the new &lt;code&gt;usage&lt;/code&gt; field, and the endpoint that moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Querying your balance at the old address now returns a 400. The response body is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET https://api.novelai.net/user/subscription → 400
{"statusCode":400,"message":"Please refresh NovelAI.net. If using a third-party tool, update to the image URL."}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that there's a policy change. The official blog post (2026-08-20) says a rechargeable battery-style usage limit applies to V5 only, refilling automatically over roughly a week, and that starting 31 days after the V5 release, unused subscription Anlas is reset to 0 when a subscription period ends. V5 launched on August 21, which puts the effective date at September 21.&lt;/p&gt;

&lt;p&gt;Image models older than V5 and the text models stay unlimited. But Anlas was visibly draining on an Opus account while generating with V5, so I needed to find out under exactly which conditions it gets charged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;On an Opus account (&lt;code&gt;tier: 3&lt;/code&gt;) I generated one image at a time with the same prompt, seed, and 28 steps, calling &lt;code&gt;GET https://image.novelai.net/user/subscription&lt;/code&gt; before and after each generation and diffing &lt;code&gt;trainingStepsLeft&lt;/code&gt;. Balance queries have to go to that host now to get a normal response.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Anlas charged&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768 (Small)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216 (Normal)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1024×1536 (Large)&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-4-5-full&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;832×1216 (Normal)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nai-diffusion-5-curated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;512×768 (Small)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Within what I measured, everything up to Normal size (832×1216) at 28 steps and one image per request was 0 Anlas, and subscription Anlas started coming out at Large (1024×1536). The ComfyUI node (&lt;code&gt;ComfyUI_NAIDGenerator&lt;/code&gt;) implements the Opus free tier as up to 1,048,576 pixels (1024×1024), up to 28 steps, and one image — I didn't measure 29 steps, multi-image batches, or the exact pixel boundary this time. Large size is where subscription Anlas gets charged, and that subscription Anlas (&lt;code&gt;fixedTrainingStepsLeft&lt;/code&gt;) is what disappears when your subscription period ends after September 21. If you only cancel renewal and still have paid time left, the clock is the end of that paid period.&lt;/p&gt;

&lt;p&gt;I queried the same account again once the effective date had passed. On 2026-09-22 the subscription Anlas read 9,898, the same figure as the September 13 sample, and &lt;code&gt;expiresAt&lt;/code&gt; was still 1790674321, which is 2026-09-29 18:32 KST. So at least on this account nothing was wiped on the effective date; it is simply still inside its paid period. That is one sample, but it rules out a blanket reset on the day itself. I'll query again after that renewal and report what the reset does.&lt;/p&gt;

&lt;p&gt;The test account held 9,961 subscription Anlas and 32 purchased Anlas (&lt;code&gt;purchasedTrainingSteps&lt;/code&gt;) as separate line items, and the 45 for the Large generation came out of subscription Anlas only (9,961 → 9,916). If the total the web UI shows doesn't match, check whether it's the sum of the two. The reset targets the subscription side; whether purchased Anlas is exempt is listed under Unverified below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz82upg1ih911rbhzodbw.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz82upg1ih911rbhzodbw.webp" alt="A V5 Full normal-size output generated for 0 Anlas on an Opus account" width="800" height="1169"&gt;&lt;/a&gt;&lt;br&gt;
The battery sits at the end of the subscription response, in a &lt;code&gt;usage&lt;/code&gt; field. It didn't move once across four V5 generations. When I queried again 46 hours later, &lt;code&gt;percent&lt;/code&gt; had dropped to 98 — after a handful of additional V5 generations from the web UI on this account in between (count not recorded). So the battery is a meter that really does go down, and &lt;code&gt;timeUntilNextPercent&lt;/code&gt; stayed at 7,888 even at 98%, so it isn't a live countdown. A third sample nine days later was back at 100%, so it does recharge, but September 19 — the date NovelAI's official account gave for the usage limit renewal, seen 2026-09-14 — falls inside that gap, so I can't tell natural recharge from the renewal. &lt;code&gt;timeUntilNextPercent&lt;/code&gt; read 7,888 in all three samples. That's as far as the evidence goes:&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="nl"&gt;"usage"&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;"percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isNegative"&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;"timeUntilNextPercent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7888&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;I checked the V5 model IDs while I was there. Both &lt;code&gt;nai-diffusion-5-full&lt;/code&gt; and &lt;code&gt;nai-diffusion-5-curated&lt;/code&gt; returned 200 with an image, and the PNG &lt;code&gt;Source&lt;/code&gt; metadata read &lt;code&gt;NovelAI Diffusion V5 0ADF9AB7&lt;/code&gt; and &lt;code&gt;NovelAI Diffusion V5 DB276663&lt;/code&gt; respectively. Sending an ID that doesn't exist returns &lt;code&gt;Validation error: model nai-diffusion-5-nonexistent doesn't exist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For reference, a Japanese write-up (note.com, 2026-08-21) put V5 generation cost at 11 Anlas for Small, 26 for Normal, and 39 for Large, while my measurement for Large was 45. That write-up doesn't state step count or account tier (the web UI default is 23 steps; my runs were 28), so it isn't directly comparable. Logged out, the web UI showed 26 Anlas for the default settings (V5 Curated, 23 steps, Normal size), which is the non-subscriber rate with no free tier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjviiq2qaepgbb4xwtvd.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjviiq2qaepgbb4xwtvd.webp" alt="NovelAI web UI while logged out. The generate button on the V5 Curated defaults shows 26 Anlas." width="648" height="1056"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Keeping Opus doesn't exempt you from the V5 battery usage limit. That said, within what I measured, the Anlas charging rules are the same as V4.5 — up to Normal size, 28 steps or fewer, one image at a time, it's 0 Anlas. Battery capacity and recharge rate are still unknown. A second sample 46 hours later showed 98%, but I didn't record how many images were generated in between, so how much use it takes to get blocked stays unverified.&lt;/li&gt;
&lt;li&gt;If you plan to cancel, order matters. After September 21, whatever subscription Anlas is left disappears when your subscription period ends, so it's better to burn the subscription side down before that period runs out — with Large generations or multi-image batches.&lt;/li&gt;
&lt;li&gt;In third-party tools, point the balance query at &lt;code&gt;image.novelai.net&lt;/code&gt;. If generation works but the balance won't load, that's almost always this.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Image: the sample output was generated with NovelAI Diffusion V5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;On Opus, V5 is still free up to Normal size at 28 steps, and if you plan to cancel, spend down your subscription Anlas before your paid period ends once the reset policy takes effect on September 21, 2026. If a tool can generate but can't read your balance, the old host is the most likely cause, so check which balance endpoint it calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unverified
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;How the subscription Anlas reset actually behaves: the effective date has passed, but this account still has paid time left, so there was nothing to observe. I'll re-query after its renewal on 2026-09-29.&lt;/li&gt;
&lt;li&gt;Battery capacity and recharge rate: three samples show it going down and coming back up, but the generations in between weren't counted and the September 19 renewal falls inside the gap, so I couldn't derive a drain or recharge rate.&lt;/li&gt;
&lt;li&gt;The reset notice text and battery display in the web UI: my browser session was logged out, so I couldn't capture the subscriber view.&lt;/li&gt;
&lt;li&gt;Whether purchased Anlas is exempt from the reset: inferred from the official post mentioning subscription Anlas only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Changelog
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;2026-09-11 first written (measured on an Opus account via API)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1&lt;/td&gt;
&lt;td&gt;2026-09-13 second battery sample added (re-queried 46 hours later)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2&lt;/td&gt;
&lt;td&gt;2026-09-22 re-queried the day after the effective date (subscription Anlas unchanged at 9,898, battery back at 100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.3&lt;/td&gt;
&lt;td&gt;2026-09-30 added the AI disclosure line (no content change)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;AI disclosure: translated and edited with AI assistance (Claude) from my Korean original. Measurements were run with Claude Code under my supervision; I reviewed the result.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>novelai</category>
      <category>ai</category>
      <category>api</category>
      <category>generativeai</category>
    </item>
  </channel>
</rss>
