<?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: orca forge</title>
    <description>The latest articles on DEV Community by orca forge (@orca_forge).</description>
    <link>https://dev.to/orca_forge</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%2F4034330%2Fc9ccc162-e897-4e55-9e1a-de55bce4bc27.png</url>
      <title>DEV Community: orca forge</title>
      <link>https://dev.to/orca_forge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/orca_forge"/>
    <language>en</language>
    <item>
      <title>Why Low-Performance Detection Stops Smartphone Dragging</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 02 Nov 2026 13:03:38 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-low-performance-detection-stops-smartphone-dragging-2g2f</link>
      <guid>https://dev.to/orca_forge/why-low-performance-detection-stops-smartphone-dragging-2g2f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/ordinary-phone-judged-low-power/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=ordinary-phone-judged-low-power" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR The &lt;code&gt;lite&lt;/code&gt; check sent some smartphones into a touch path that did not update pages during a slow drag. It used CPU core count, device memory, or the data saver setting to classify a device as low performance. Changing the threshold and removing that touch-path branch made drag updates work in a simulated four-core, 4 GB environment and on a real device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The screen did not move when slowly dragging on a smartphone because JavaScript classified the device as low-powered and did not use the process that swaps pages in response to finger movement. After adjusting the low-performance threshold and touch handling branch so that every device uses dragging, I confirmed the behavior in both a test environment simulating 4 cores and 4 GB of memory and on a real device.&lt;/p&gt;

&lt;h2&gt;
  
  
  At First, I Suspected Browser Scrolling
&lt;/h2&gt;

&lt;p&gt;At 6:13 p.m. on October 10, I added a feature that lets you switch slides by dragging your finger. A few hours later, someone who tried it on a real device sent in a report.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I’m trying it on my phone, but the opacity doesn’t change at all. There’s a hitch while scrolling, and then the next page appears.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My first thought was that the smartphone browser was handling vertical scrolling before anything else. It looked as though the slide extended one pixel past the bottom of the screen, leading the browser to treat the page as scrollable.&lt;/p&gt;

&lt;p&gt;So I removed the overflow and routed all vertical finger movement through a script. For longer content, the script would add inertia to the scrolling. In other words, I wouldn’t leave scrolling to the browser.&lt;/p&gt;

&lt;p&gt;But the next report was even worse:&lt;/p&gt;

&lt;p&gt;“Even when I scroll up and down, the screen doesn’t move at all.”&lt;/p&gt;

&lt;p&gt;If I stopped the browser from scrolling, the drag should have moved the screen. In reality, nothing happened, even when the person moved their finger slowly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did Dragging Stop When the Device Was Classified as Low-End?
&lt;/h2&gt;

&lt;p&gt;The cause was the criteria used to classify a device as low-end. &lt;code&gt;hardwareConcurrency&lt;/code&gt; is the number of logical CPU cores reported by the browser, and &lt;code&gt;deviceMemory&lt;/code&gt; is an estimate of the device’s memory.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hardwareConcurrency&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceMemory&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceMemory&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;saveData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the device has four or fewer CPU cores, 4 GB or less of memory, or data saver is enabled, &lt;code&gt;lite&lt;/code&gt; becomes true. Even ordinary smartphones met these criteria.&lt;/p&gt;

&lt;p&gt;On devices classified as low-end, the app didn’t use the logic that updates the screen in sync with a drag. It switched to advancing to the next item all at once only when you swiped quickly.&lt;/p&gt;

&lt;p&gt;So the lack of response to a slow drag wasn’t caused by missed touch input. The app simply wasn’t running the logic that updates the screen in response to the touch movement. The first fix stopped relying on the browser to scroll, exposing that gap: even a slow drag no longer moved the page. The snag reported at 00:19 was also related to the same device classification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Problem Didn’t Reproduce on a Computer
&lt;/h2&gt;

&lt;p&gt;I was checking the behavior on a computer with many CPU cores. Even when I mimicked a smartphone’s screen size and touch input, it didn’t meet the criteria for a low-performance device. I could match the screen size and touch behavior, but the &lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt; and &lt;code&gt;navigator.deviceMemory&lt;/code&gt; conditions were different.&lt;/p&gt;

&lt;p&gt;As a result, drag-and-drop reordering worked on the computer, while the smartphone took a different code path. The issue didn’t appear in my test environment; it only showed up on a real device. Simply shrinking the screen and enabling touch input wasn’t enough to reproduce the device detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Made Dragging Work Across Devices
&lt;/h2&gt;

&lt;p&gt;At 01:55, I fixed the conditional that caused the issue. I changed the low-performance threshold from 4 to 2 and removed the condition that treated low-performance devices as swipes in the touch handler. Now every device uses drag behavior.&lt;/p&gt;

&lt;p&gt;Before the fix, this condition turned the gesture into a swipe:&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="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="nx"&gt;lite&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;busyUntil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;touch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;swipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I removed this branch and made the touch handler set &lt;code&gt;touch.mode = 'drag'&lt;/code&gt; regardless of the device. This reflects the distance your finger moves on screen instead of relying only on the logic that triggers when you flick quickly.&lt;/p&gt;

&lt;p&gt;I also added a test to verify the fix. It mocks &lt;code&gt;navigator&lt;/code&gt; as &lt;code&gt;{ hardwareConcurrency: 4, deviceMemory: 4 }&lt;/code&gt; and slowly drags in 20px increments. The test count increased from 31 to 32.&lt;/p&gt;

&lt;p&gt;In an environment simulating 4 cores and 4 GB of memory, dragging 200px shifted the cover 79% of the way and brought the next page 21% into view. When I lifted my finger, the cover returned to its original position. Dragging 450px shifted the cover 61% of the way; when I lifted my finger, it advanced to page 2. These checks confirmed that the intermediate drag position appeared on screen even with values that trigger the low-performance classification.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Learned After It Worked on a Real Device
&lt;/h2&gt;

&lt;p&gt;At 02:41, someone who had confirmed it was working on a real device reported, “The effect now works on smartphones.” The initial report made it clear that fixing browser scrolling alone wasn’t enough. The issue was that different devices were routed to different processing paths.&lt;/p&gt;

&lt;p&gt;When treated as a low-performance device, the smartphone also missed out on the animated heading assembly and 3D display. After the device detection was fixed, both appeared on smartphones too.&lt;/p&gt;

&lt;p&gt;However, we haven’t confirmed how performance was rated on the smartphone after the fix, and we don’t know which models were affected. Confirming that the animation works is separate from confirming that performance is acceptable on every device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the conditions and thresholds that make &lt;code&gt;lite&lt;/code&gt; true.&lt;/li&gt;
&lt;li&gt;Check whether touch handling routes low-performance devices away from drag updates.&lt;/li&gt;
&lt;li&gt;Test with &lt;code&gt;navigator&lt;/code&gt; set to &lt;code&gt;{ hardwareConcurrency: 4, deviceMemory: 4 }&lt;/code&gt; and drag slowly in 20px steps.&lt;/li&gt;
&lt;li&gt;Check drag progress on a real device as well as with simulated screen size and touch input.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
    </item>
    <item>
      <title>Why PDF Emoji Show Broken Image Icons: Causes and Fixes</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sun, 01 Nov 2026 13:03:16 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-pdf-emoji-show-broken-image-icons-causes-and-fixes-3bkk</link>
      <guid>https://dev.to/orca_forge/why-pdf-emoji-show-broken-image-icons-causes-and-fixes-3bkk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/marp-emoji-broken-image-offline/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=marp-emoji-broken-image-offline" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR In an offline container, emoji were handled as CDN images and could not be fetched. Adding a TTF font made the glyph visible, but the broken image icon remained. Setting emoji not to convert to images and passing the config with &lt;code&gt;--config-file&lt;/code&gt; removed the icon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;In a container without network access, emojis were treated as images served from a CDN (a system for delivering images from an external source). Because those images couldn’t be fetched, the PDF showed broken image markers. Adding a TTF (font file format) displayed the glyphs, but the image retrieval errors remained. Adding a setting to prevent emojis from being converted to images and passing it with &lt;code&gt;--config-file&lt;/code&gt; removed the markers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did the Broken Image Marker Remain After I Added a Font?
&lt;/h2&gt;

&lt;p&gt;In the previous post, I wrote about PDF generation stopping when JavaScript was disabled. This time, I was trying to display emoji in a container with no network access.&lt;/p&gt;

&lt;p&gt;First, I added a font. At 15:02:41, I deployed an emoji TTF into a new image layer. &lt;code&gt;fc-cache&lt;/code&gt; updates font information, but I didn’t run it. The font was picked up by checking the font directory’s mtime (modification time). I used &lt;code&gt;fc-match&lt;/code&gt; with &lt;code&gt;charset=1f539&lt;/code&gt; and confirmed that it selected the target glyph.&lt;/p&gt;

&lt;p&gt;I thought that would fix the PDF too. But the PDF created at 17:33:36 was 301,735 bytes. The glyph for U+1F539 appeared, but the “broken image” marker remained just before it.&lt;/p&gt;

&lt;p&gt;At first, I thought the font hadn’t loaded correctly. But the glyph was in the PDF. Looking only at the font couldn’t explain the marker. The cause was how the emoji were rendered. The initial rendering configuration used images hosted on a CDN for emoji. Because the container had no network access, it couldn’t fetch the image, so the PDF generator displayed a broken image marker before the glyph.&lt;/p&gt;

&lt;p&gt;Fonts and images take separate paths. Adding a font for the glyph doesn’t solve the problem of being unable to fetch an external image.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which setting stopped emoji from being rendered as images
&lt;/h2&gt;

&lt;p&gt;Next, I changed how emojis were handled in the configuration. In the 17:47 commit, I enabled shortcodes (a notation for specifying emojis and other items by short names) and added a setting to prevent Unicode characters from being converted into images. I passed the configuration file to PDF generation with &lt;code&gt;--config-file&lt;/code&gt;. I deployed it at 17:50.&lt;/p&gt;

&lt;p&gt;The PDF generated at 22:25 was 312,020 bytes. This time, the broken image markers were gone, and the person who requested it reviewed the PDF and approved it.&lt;/p&gt;

&lt;p&gt;In the previous article, disabling JavaScript stopped the process itself. This time, the process ran and produced a PDF. Even so, if the setting to treat emojis as external images remained, the images could not be fetched in an environment without network access. The fact that a PDF was generated alone was not enough to tell whether its contents were correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are face emoji still monochrome?
&lt;/h2&gt;

&lt;p&gt;Not every emoji is fixed. Face emoji such as &lt;code&gt;:smile:&lt;/code&gt; are still monochrome because DejaVu Sans is selected before Noto Color Emoji. Possible fixes include configuring fontconfig, which controls font selection, or setting &lt;code&gt;font-family&lt;/code&gt; in CSS, which controls how a web page looks. I left this unresolved for now and moved on.&lt;/p&gt;

&lt;p&gt;To try generating a real PDF locally, you need &lt;code&gt;CHROME_NO_SANDBOX=1&lt;/code&gt;. A long &lt;code&gt;TMPDIR&lt;/code&gt; value also causes &lt;code&gt;SingletonSocket&lt;/code&gt; to crash. These issues are separate from emoji rendering, but they’re worth knowing before trying to reproduce the problem.&lt;/p&gt;

&lt;p&gt;What I confirmed this time is that adding a font can make glyphs appear while a separate failure remains if the emoji is fetched as an image. I inspected the PDF contents and traced glyphs and image markers separately. Face emoji are still a task for another day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the glyph and broken image icon separately in the output PDF.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;fc-match&lt;/code&gt; with &lt;code&gt;charset=1f539&lt;/code&gt; to check whether the target glyph is selected.&lt;/li&gt;
&lt;li&gt;Configure emoji not to convert to images and pass the file with &lt;code&gt;--config-file&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Open the PDF to confirm the icon is gone; face emoji may remain monochrome.&lt;/li&gt;
&lt;li&gt;For local reproduction, set &lt;code&gt;CHROME_NO_SANDBOX=1&lt;/code&gt; and check whether a long &lt;code&gt;TMPDIR&lt;/code&gt; causes &lt;code&gt;SingletonSocket&lt;/code&gt; to fail.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>pdf</category>
    </item>
    <item>
      <title>Why Chromium PDF Conversion Times Out When JavaScript Is Disabled</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sat, 31 Oct 2026 13:00:07 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-chromium-pdf-conversion-times-out-when-javascript-is-disabled-3937</link>
      <guid>https://dev.to/orca_forge/why-chromium-pdf-conversion-times-out-when-javascript-is-disabled-3937</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/chromium-script-disabled-pdf-hangs/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=chromium-script-disabled-pdf-hangs" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR Chromium PDF conversion stopped because JavaScript was disabled with &lt;code&gt;--blink-settings=scriptEnabled=false&lt;/code&gt;. marp-cli 4.5.1 waited for &lt;code&gt;requestAnimationFrame&lt;/code&gt;, which did not return with scripts disabled. Removing the setting from both the Marp and direct HTML paths restored PDF output in about 4 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Chromium’s PDF conversion stalled when JavaScript was disabled with &lt;code&gt;--blink-settings=scriptEnabled=false&lt;/code&gt;. marp-cli 4.5.1 waits for the result of &lt;code&gt;requestAnimationFrame&lt;/code&gt;, but that value was never returned while scripts were disabled. After removing the setting from both Marp and the raw HTML, I verified the fix in real Chromium: the PDF was generated in about four seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Released After a Full Build Without Checking PDF Conversion
&lt;/h2&gt;

&lt;p&gt;A full build had replaced dependencies that weren’t pinned to specific versions. marp-cli changed from 4.3.1 to 4.5.1, puppeteer-core from 24.42.0 to 24.43.1, and Chromium was now version 154.&lt;/p&gt;

&lt;p&gt;Before release, I tested only the authentication endpoints. I confirmed they returned 401 and 403. I didn’t run PDF conversion. I released with the dependencies changed, without verifying the conversion.&lt;/p&gt;

&lt;p&gt;At first, I suspected the code that invoked the conversion. Nothing appeared on screen. When I investigated, I found that it called &lt;code&gt;subprocess.run&lt;/code&gt; inside an async operation. Because the synchronous call blocked the event loop—the mechanism that advances asynchronous work in order—I couldn’t see the conversion’s progress either. Meanwhile, Liveness—the monitoring check that verifies the service is running—was terminating the container.&lt;/p&gt;

&lt;p&gt;“If the process is getting stuck, I just need to fix how the child process is started.” That was my conclusion. At least, it fit part of the symptoms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a 60-Second Limit to the Child Process Still Results in a 504
&lt;/h2&gt;

&lt;p&gt;At 11:55, I made the first fix. I ran the child process asynchronously and added a 60-second timeout. To make sure any grandchildren it spawned were also cleaned up, I used a subreaper—a mechanism that collects a process’s children.&lt;/p&gt;

&lt;p&gt;I also changed how it was invoked: I ran marp directly with &lt;code&gt;--no-stdin --browser-timeout 30&lt;/code&gt;. All 228 tests passed.&lt;/p&gt;

&lt;p&gt;I expected this to return the PDF. Instead, it still timed out with a 504 after 60 seconds.&lt;/p&gt;

&lt;p&gt;Adding a timeout doesn’t necessarily end the operation that’s still waiting. Even after making it possible to stop the child process, I still had to investigate what it was waiting for.&lt;/p&gt;

&lt;p&gt;I suspected the network restriction might be the cause. Since Chromium’s outbound traffic was blocked, perhaps that was holding up rendering. I investigated that possibility, but the network restriction wasn’t the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chromium Was Waiting for &lt;code&gt;requestAnimationFrame&lt;/code&gt; to Return
&lt;/h2&gt;

&lt;p&gt;The real cause was a Chromium setting we had added as a security fix:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--blink-settings=scriptEnabled=false&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This setting disables scripts in Chromium. After calling &lt;code&gt;setContent&lt;/code&gt;, marp-cli 4.5.1 calls &lt;code&gt;page.evaluate(requestAnimationFrame)&lt;/code&gt; and waits for &lt;code&gt;requestAnimationFrame&lt;/code&gt; to run at the next rendering opportunity. There is no timeout. With scripts disabled, no result is returned, so the conversion stalled there.&lt;/p&gt;

&lt;p&gt;I reproduced this with my local Chrome 151 and confirmed the cause. I also confirmed that blocking network access was not the cause.&lt;/p&gt;

&lt;p&gt;At this point, the cause was the combination of the application and the Chromium conversion process. We had disabled scripts while waiting for a value returned by a script. Adding a timeout would only have limited how long it waited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing PDF Conversion in the Same Sandbox as Production
&lt;/h2&gt;

&lt;p&gt;To reproduce the issue, I didn’t just run the command outside the sandbox. I used the same invocation as the app, including the sandbox wrapper, just as in production. That reproduced the state where PDF conversion stalls.&lt;/p&gt;

&lt;p&gt;This was important. A test that launches Chromium on its own can’t account for the settings the app passes in. The fact that a PDF was generated locally doesn’t tell you whether it will work with the actual invocation.&lt;/p&gt;

&lt;p&gt;Fixing only the Marp path wasn’t enough. The path that handles HTML directly had the same &lt;code&gt;scriptEnabled=false&lt;/code&gt; setting. Also, &lt;code&gt;--print-to-pdf&lt;/code&gt; won’t write a PDF when scripts are disabled. I had to fix each place where the problematic setting remained.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDF generation restored after fixing Marp and direct HTML settings
&lt;/h2&gt;

&lt;p&gt;At 13:27, I made a second fix. I removed &lt;code&gt;scriptEnabled=false&lt;/code&gt; from both Marp and direct HTML. I also changed the &lt;code&gt;TMPDIR&lt;/code&gt; environment variable (a value set in the runtime environment) so each request uses a separate temporary directory.&lt;/p&gt;

&lt;p&gt;I added tests using real Chromium, bringing the total to 236. Network isolation, sanitization, and CSP remain in place. I didn’t remove all the security measures; I removed the setting that was preventing PDF output and tested the result in real Chromium with the remaining measures enabled.&lt;/p&gt;

&lt;p&gt;At 14:48, the PDF was generated. It was 253,841 bytes, and conversion took about four seconds.&lt;/p&gt;

&lt;p&gt;I haven’t checked whether conversion was also blocked in the previous version. I can record that the dependency version increased and that this incident occurred, but I can’t say the version increase caused the failure. I also don’t know whether anyone used PDFs while the previous version was in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  After a Full Build, Verify the Actual PDF Conversion
&lt;/h2&gt;

&lt;p&gt;In this release check, I only looked at authentication errors (401/403) and never ran the PDF conversion. I also didn’t test with the production configuration that invokes the conversion.&lt;/p&gt;

&lt;p&gt;To prevent this from happening again, I’ve decided to run the actual process at least once after a full build. I’ll also compare versions before and after bundling the dependencies. An endpoint responding and the final artifact being produced need to be verified separately.&lt;/p&gt;

&lt;p&gt;Adding a 60-second timeout was necessary. Even so, no PDF was produced. Preventing a process from getting stuck and confirming that it completes correctly under the right conditions are separate tasks.&lt;/p&gt;

&lt;p&gt;A command may run outside the sandbox without behaving the same way it does when invoked in production. Next time, I’ll run the actual conversion after a full build. That way, I won’t treat a successful run outside the sandbox as proof that everything works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Run an actual PDF conversion before release, alongside authentication checks.&lt;/li&gt;
&lt;li&gt;Reproduce with the application’s invocation and isolation wrapper.&lt;/li&gt;
&lt;li&gt;Check both Marp and direct HTML paths for &lt;code&gt;--blink-settings=scriptEnabled=false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare dependency versions after a full build and verify that a PDF is produced.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>chromium</category>
      <category>puppeteer</category>
      <category>marp</category>
      <category>pdf</category>
    </item>
    <item>
      <title>How to Check Git State Without Misreading Command Results</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:02:29 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-check-git-state-without-misreading-command-results-4nek</link>
      <guid>https://dev.to/orca_forge/how-to-check-git-state-without-misreading-command-results-4nek</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/git-checks-that-mislead/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=git-checks-that-mislead" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR Git output alone may not show whether a path exists or a command succeeded. The command that ran and the paths being compared can also change how results should be read. This article reproduces cases involving &lt;code&gt;rev-parse&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, stash, and &lt;code&gt;git status&lt;/code&gt; in a disposable repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;When checking Git, a non-empty output or a record count alone may not tell you whether the target exists or whether the command succeeded. Check the exit code, the command that ran, and the paths being compared. If you’re using timestamps as evidence, check the state with &lt;code&gt;GIT_OPTIONAL_LOCKS=0&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check whether a target exists with &lt;code&gt;rev-parse&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;You can’t determine whether a path exists just because &lt;code&gt;git rev-parse&lt;/code&gt; (a command that resolves Git names and paths) produced non-empty output. In the example below, the specified path does not exist in &lt;code&gt;HEAD&lt;/code&gt; (the name that refers to the current commit).&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;$ v&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="s2"&gt;"HEAD:存在しないpath"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&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="nv"&gt;$v&lt;/span&gt;&lt;span class="s2"&gt;] exit=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
値&lt;span class="o"&gt;=[&lt;/span&gt;HEAD:存在しないpath] &lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unresolved argument is printed to standard output as-is. The exit code (a number indicating whether the command succeeded) is 128, but the variable &lt;code&gt;v&lt;/code&gt; is not empty. So checking only &lt;code&gt;[ -n "$v" ]&lt;/code&gt; would lead you to conclude that the path exists.&lt;/p&gt;

&lt;p&gt;To check whether it exists, look at the command’s exit code rather than whether it produced output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git cat-file &lt;span class="nt"&gt;-e&lt;/span&gt; &amp;lt;rev&amp;gt;:&amp;lt;path&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the reproduction I recorded, the exit code was 128 when the path did not exist and 0 when it did. To list a tree’s contents for inspection, use &lt;code&gt;git ls-tree&lt;/code&gt;. What matters is whether Git could resolve the specified target, not whether it printed anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check when &lt;code&gt;grep&lt;/code&gt; appears to return 0 matches or an error
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;grep&lt;/code&gt; appears to find 0 matches or returns an error, check what is actually being invoked. In one environment, a shell function named &lt;code&gt;grep&lt;/code&gt; was overriding the command and calling a different grep implementation.&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;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; alpha s.txt
error: unknown option &lt;span class="s1"&gt;'-G'&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;command grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; alpha s.txt
1
&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file contains one match for &lt;code&gt;alpha&lt;/code&gt;. But the first invocation returns an error and an exit code of 1. If you interpret exit code 1 as “no matches,” you’ll miss a match that’s right there. The command meant to check the result can end up hiding the very state you’re trying to verify.&lt;/p&gt;

&lt;p&gt;You can check what will be invoked with this command:&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;type grep&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To invoke the regular command instead of a shell function, use &lt;code&gt;command grep&lt;/code&gt;. You can also call it by its absolute path. Before checking the match count, make sure the &lt;code&gt;grep&lt;/code&gt; you ran is the one you expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are You Counting When Comparing Stash Differences?
&lt;/h2&gt;

&lt;p&gt;When checking a stash (Git’s feature for temporarily saving work), state what you’re comparing. &lt;code&gt;stash@{0}&lt;/code&gt; is the working tree (the set of files you were working on) at the time of the stash. &lt;code&gt;stash@{0}^2&lt;/code&gt; is the index (the staged content) at that time, &lt;code&gt;stash@{0}^1&lt;/code&gt; is HEAD, and &lt;code&gt;stash@{0}^3&lt;/code&gt; is untracked files.&lt;/p&gt;

&lt;p&gt;In one comparison, the entire tree had differences across 3 files and 434 lines. From that, I concluded, “A stash isn’t a reliable backup.” But those 3 files weren’t among the 20 items I was checking.&lt;/p&gt;

&lt;p&gt;After comparing file contents path by path, the 20 items broke down as follows: 15 were identical, 5 were deleted in both places, and none differed. For those 20 items, the stash was a complete backup.&lt;/p&gt;

&lt;p&gt;The numbers differed because I wasn’t counting the same items. Before revisiting the size of the differences, check that the comparison covers the same scope. Stating upfront “which paths, in what two things, are being counted” makes it easier to interpret the numbers correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can the timestamp change after &lt;code&gt;git status&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;In the environment I recorded, &lt;code&gt;git status&lt;/code&gt;, which I intended to use for reading only, changed the index mtime (file modification time).&lt;/p&gt;

&lt;p&gt;When timestamps matter as evidence, use this instead of the usual &lt;code&gt;git status&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="nv"&gt;GIT_OPTIONAL_LOCKS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 git &lt;span class="nt"&gt;-C&lt;/span&gt; &amp;lt;path&amp;gt; status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A command that displays state does not necessarily leave files unchanged. If you plan to treat timestamps as evidence after checking, consider whether the command itself could affect them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide which path or file you want to check.&lt;/li&gt;
&lt;li&gt;Check the exit code as well as the output.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;type grep&lt;/code&gt; to see whether a shell function replaces the command.&lt;/li&gt;
&lt;li&gt;Compare the same paths and file-count scope.&lt;/li&gt;
&lt;li&gt;For timestamps, check state with &lt;code&gt;GIT_OPTIONAL_LOCKS=0 git -C &amp;lt;path&amp;gt; status --porcelain&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Check OpenHands Docker Port Exposure and Fix It</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Thu, 29 Oct 2026 13:03:28 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-check-openhands-docker-port-exposure-and-fix-it-1739</link>
      <guid>https://dev.to/orca_forge/how-to-check-openhands-docker-port-exposure-and-fix-it-1739</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/openhands-cve-2026-112346-docker-port-check/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=openhands-cve-2026-112346-docker-port-check" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR: Docker port publishing without a host IP can expose a port on all host interfaces. CVE-2026-112346 could allow unauthenticated remote code execution in the sandbox when that port is reachable. Check the installed version, bind the host port to &lt;code&gt;127.0.0.1&lt;/code&gt;, and configure the session API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Check the Installed Version
&lt;/h2&gt;

&lt;p&gt;First, check the versions of the related packages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip show openhands-sdk openhands-workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;According to the CVE description, versions of the OpenHands Software Agent SDK earlier than 1.52.0 are affected. The fix was released in version 1.52.0. If you’re using a version earlier than 1.52.0, update it. &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-112346" rel="noopener noreferrer"&gt;NVD&lt;/a&gt; &lt;a href="https://github.com/OpenHands/software-agent-sdk/releases/tag/v1.52.0" rel="noopener noreferrer"&gt;v1.52.0 release&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To update the packages, update at least the following packages according to the procedures for your environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; openhands-sdk openhands-workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your environment uses dependencies or a lockfile, update them using your usual process, then run &lt;code&gt;pip show&lt;/code&gt; again to confirm the installed versions.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Check Docker’s Published Ports
&lt;/h2&gt;

&lt;p&gt;Next, list the running containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the &lt;code&gt;PORTS&lt;/code&gt; column in the output. For example, an entry like this means the port is published on all host interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0.0.0:36957-&amp;gt;8000/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it starts with &lt;code&gt;127.0.0.1&lt;/code&gt;, the port is published only on the host’s loopback interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;127.0.0.1:36957-&amp;gt;8000/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also check an individual port with this command. Replace &lt;code&gt;&amp;lt;コンテナ&amp;gt;&lt;/code&gt; (container) with the name or ID of the container you want to inspect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker port &amp;lt;コンテナ&amp;gt; 8000/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the output starts with &lt;code&gt;127.0.0.1:...&lt;/code&gt;, it’s bound to loopback. If the host-side address is something like &lt;code&gt;0.0.0.0&lt;/code&gt;, check the scope of the published port. In the verification results for the fix PR, this command output was &lt;code&gt;127.0.0.1:36957&lt;/code&gt;. &lt;a href="https://github.com/OpenHands/software-agent-sdk/pull/5209" rel="noopener noreferrer"&gt;PR #5209&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Bind the Published Port to Loopback
&lt;/h2&gt;

&lt;p&gt;When you specify &lt;code&gt;docker run&lt;/code&gt; yourself, &lt;code&gt;-p &amp;lt;host-port&amp;gt;:&amp;lt;container-port&amp;gt;&lt;/code&gt; publishes the port on all interfaces if you omit the host IP address. To bind it only to the host’s loopback interface, specify &lt;code&gt;127.0.0.1&lt;/code&gt; as the host IP. &lt;a href="https://github.com/OpenHands/software-agent-sdk/pull/5209" rel="noopener noreferrer"&gt;PR #5209&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:&amp;lt;host-port&amp;gt;:&amp;lt;container-port&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After updating the startup configuration this way, restart the container and check the &lt;code&gt;PORTS&lt;/code&gt; column in &lt;code&gt;docker ps&lt;/code&gt; and the output of &lt;code&gt;docker port&lt;/code&gt; again. Confirm that the output starts with &lt;code&gt;127.0.0.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the OpenHands fix, the server inside the container still uses &lt;code&gt;--host 0.0.0.0&lt;/code&gt;. The loopback restriction belongs in the host-side port publishing configuration, not inside the container. The PR description explains that the server must listen on &lt;code&gt;0.0.0.0&lt;/code&gt; inside the container so Docker’s bridge forwarding can reach it. &lt;a href="https://github.com/OpenHands/software-agent-sdk/pull/5209" rel="noopener noreferrer"&gt;PR #5209&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Set the Session API Key
&lt;/h2&gt;

&lt;p&gt;The CVE description says authentication was disabled by default when &lt;code&gt;session_api_keys&lt;/code&gt; was empty. Set a session API key according to your environment’s configuration, and make sure both the client and server use the same key. &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-112346" rel="noopener noreferrer"&gt;NVD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The DockerWorkspace fix changed the client to check &lt;code&gt;OH_SESSION_API_KEYS_0&lt;/code&gt; first and fall back to &lt;code&gt;SESSION_API_KEY&lt;/code&gt;, matching the server’s precedence. Check the actual setting name and how to pass it based on your startup configuration. &lt;a href="https://github.com/OpenHands/software-agent-sdk/pull/5209" rel="noopener noreferrer"&gt;PR #5209&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Make sure to check both that the port is bound to loopback and that an API key is set. The former limits exposure on the host side; the latter is relevant to session API authentication.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an Agent Sandbox Can Cause Greater Damage
&lt;/h2&gt;

&lt;p&gt;An agent sandbox is a place where AI can run shell commands and read and write files. It is designed to let the agent do anything it needs inside, so if someone on the outside can reach it without authentication, that same “anything” is handed directly to them. The CVE description lists what an attacker could do: run commands as root, access the mounted workspace, and read plaintext secrets from their storage location. &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-112346" rel="noopener noreferrer"&gt;NVD&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The workspace will often contain the repository you’re working on, and the secrets may include LLM API keys. The sandbox protects the host; it does not protect what’s inside the sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You Use Apptainer
&lt;/h2&gt;

&lt;p&gt;The CVE description says Apptainer workspaces are also affected. However, the Docker fix PR targets &lt;code&gt;DockerWorkspace&lt;/code&gt; and explicitly states that other workspace types, such as &lt;code&gt;ApptainerWorkspace&lt;/code&gt;, are not changed. &lt;a href="https://nvd.nist.gov/vuln/detail/CVE-2026-112346" rel="noopener noreferrer"&gt;NVD&lt;/a&gt; &lt;a href="https://github.com/OpenHands/software-agent-sdk/issues/5379" rel="noopener noreferrer"&gt;issue #5379&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you use Apptainer, set a session API key and make sure the port is not reachable from outside. Don’t infer Apptainer’s status from the Docker fix alone; check your own launch configuration and which networks expose the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check &lt;code&gt;openhands-sdk&lt;/code&gt; and &lt;code&gt;openhands-workspace&lt;/code&gt;; update versions earlier than 1.52.0.&lt;/li&gt;
&lt;li&gt;Inspect &lt;code&gt;docker ps&lt;/code&gt; and &lt;code&gt;docker port&lt;/code&gt; for host port bindings.&lt;/li&gt;
&lt;li&gt;Bind the host side to &lt;code&gt;127.0.0.1&lt;/code&gt; and verify the mapping after restarting the container.&lt;/li&gt;
&lt;li&gt;Configure the same session API key on the client and server.&lt;/li&gt;
&lt;li&gt;For Apptainer, inspect the actual launch settings and network exposure; the DockerWorkspace fix does not establish its state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/litellm-1-107-0-compromise-check-mac/" rel="noopener noreferrer"&gt;LiteLLM 1.107.0 Tampering: How to Check Whether Your Mac Is Affected&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/tailscale-ssh-bypasses-authorized-keys/" rel="noopener noreferrer"&gt;Tailscale SSH Bypassed the &lt;code&gt;authorized_keys&lt;/code&gt; Restrictions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>docker</category>
      <category>ai</category>
      <category>cve</category>
    </item>
    <item>
      <title>Why Git stash pop left old changes in a shared worktree</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:03:34 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-git-stash-pop-left-old-changes-in-a-shared-worktree-4oac</link>
      <guid>https://dev.to/orca_forge/why-git-stash-pop-left-old-changes-in-a-shared-worktree-4oac</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/stash-pop-wrote-back-the-past/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=stash-pop-wrote-back-the-past" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR A shared worktree had staged changes across 20 files, with 56 insertions and 1,305 deletions. The files and commit history showed that the changes reflected a state from about three weeks earlier. I preserved the old state on a branch, then used &lt;code&gt;reset --hard&lt;/code&gt; and &lt;code&gt;pull --ff-only&lt;/code&gt; to update the worktree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conclusion first
&lt;/h2&gt;

&lt;p&gt;What remained in the shared working tree after &lt;code&gt;git stash pop&lt;/code&gt; was an old state from about three weeks earlier, not new edits. Stashing the staged changes did not update the files in the working tree, so after stashing them, I ran &lt;code&gt;reset --hard&lt;/code&gt; and &lt;code&gt;pull --ff-only&lt;/code&gt;. I couldn’t determine why the fourth stash attempt was the only one that failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Didn’t Mistake the Diff for New Edits
&lt;/h2&gt;

&lt;p&gt;First, I compared the staged content with the files on disk. The index is where changes are held before they go into the next commit. There were no differences between the working tree and the index, and there were no untracked files.&lt;/p&gt;

&lt;p&gt;In other words, this wasn’t a case of old changes lingering only in the index. The files on disk themselves were in an old state. The files shown as deleted in the diff were also missing from disk.&lt;/p&gt;

&lt;p&gt;So I stopped treating the staged content as new work. All three missing files had been incorporated by two days before the incident. They covered the test harness, usage measurement, and context hints.&lt;/p&gt;

&lt;p&gt;The main branch’s HEAD (the name pointing to the current commit) had been stuck for about three weeks since the merge commit on the day of the incident. The size of the diff alone didn’t explain what had happened. By looking at the commit timeline alongside whether the files existed, I could tell this was a leftover from an earlier state, not a recent edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Stashing Alone Doesn’t Restore the Workspace
&lt;/h2&gt;

&lt;p&gt;If you stash the changes and stop there, the main working tree still contains the old tree. A build from that state will use the old version again, causing the next rollback.&lt;/p&gt;

&lt;p&gt;Stashing is a way to preserve the leftovers without losing them. It is separate from restoring the working tree to its current state. Running only one of these steps leaves the shared working tree unusable for the next task.&lt;/p&gt;

&lt;p&gt;I decided to treat preserving the old state and updating the main working tree to the latest version as a single recovery process.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Preserved the Old Changes and Updated the Main Branch
&lt;/h2&gt;

&lt;p&gt;First, I saved the remaining work as a commit on a named branch, which serves as a marker for a separate line of work. Keeping the backup branch means the contents can be reviewed later. I did not delete it.&lt;/p&gt;

&lt;p&gt;Next, I ran &lt;code&gt;reset --hard&lt;/code&gt; to align the working tree with HEAD. Then I updated it with &lt;code&gt;pull --ff-only&lt;/code&gt;. The result was a 196-commit fast-forward, with no merge commit.&lt;/p&gt;

&lt;p&gt;I also kept the stash instead of deleting it. This let me preserve the old contents while moving the main working tree forward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Did &lt;code&gt;stash pop&lt;/code&gt; Fail Only on the Fourth Try?
&lt;/h2&gt;

&lt;p&gt;From what I heard later, the same steps succeeded three times that day and failed for the first time on the fourth. I was told that the failure displayed &lt;code&gt;The stash entry is kept&lt;/code&gt;. The reason it failed only on the fourth try could not be determined, so I can’t claim to know the cause.&lt;/p&gt;

&lt;p&gt;What I could confirm this time was that the shared working tree still contained a state from about three weeks earlier, and that the files on disk were stale as well as the index. I also learned that recovery isn’t complete just by stashing the changes.&lt;/p&gt;

&lt;p&gt;“Restoring” by preserving the old state and “updating” by bringing the main working tree up to date can be treated as separate steps. But in a shared working tree, recovery meant completing both. After stashing the changes, I updated the main tree, so we didn’t move on to the next build with the old tree still in place.&lt;/p&gt;

&lt;p&gt;I’ll write about the caveats of tools for checking the state in a separate article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the diff size with &lt;code&gt;git diff --shortstat&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the worktree and index differences, and look for untracked files.&lt;/li&gt;
&lt;li&gt;Compare file presence, when changes were integrated, and HEAD.&lt;/li&gt;
&lt;li&gt;Preserve old changes on a branch, then run &lt;code&gt;reset --hard&lt;/code&gt; and &lt;code&gt;pull --ff-only&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep the branch and stash for later inspection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/unquoted-command-substitution-guard/" rel="noopener noreferrer"&gt;With unquoted &lt;code&gt;$()&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; won’t stop you&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>git</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why gpu-unlock Speeds Up RTX 3090 Prefill but Barely Changes Decode</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:03:59 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-gpu-unlock-speeds-up-rtx-3090-prefill-but-barely-changes-decode-4572</link>
      <guid>https://dev.to/orca_forge/why-gpu-unlock-speeds-up-rtx-3090-prefill-but-barely-changes-decode-4572</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/rtx3000-fp16-bf16-unlock-gpu-unlock/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=rtx3000-fp16-bf16-unlock-gpu-unlock" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR gpu-unlock changes FP16/BF16 instruction throughput on some RTX 3000-series GeForce cards. An RTX 3090 report measured 33% faster vLLM prefill and 24% less time to the first token. Decode changed by only 1.2%, because it is limited by memory bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Being Held Back
&lt;/h2&gt;

&lt;p&gt;According to the README, the affected cards are RTX 3000 series GPUs based on GA102, GA103, GA104, and GA106. It says the unlock doubles the instruction issue rate for FP16/BF16 multiply-accumulate operations using FMA, where the results are accumulated in FP32. In the README’s words, this makes them “as fast as their professional equivalents.” It does not speed up the whole GPU; only this type of operation gets faster. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;According to the explanation of how it works, the signed firmware that runs during Ampere startup—the Booter running on SEC2—has a buffer overflow vulnerability. This can be used to execute code via ROP. Through that path, the module changes write permissions for registers that drivers normally cannot write to, then changes the value that controls the instruction issue rate. The rate is selected using a 4-bit value, with options such as full speed and half speed. &lt;a href="https://github.com/Zagitta/gpu-unlock/blob/main/docs/How%20Ampere%20was%20unlocked.md" rel="noopener noreferrer"&gt;How it works&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The module is loaded before the NVIDIA driver connects to the card. It changes the setting, performs an FLR (a per-card reset), and then hands control to the unmodified driver. It is neither a UEFI app nor a modified driver. For that reason, the README says there is no need to reapply patches every time a new driver comes out. The module only runs when the target card is specified on the kernel command line; removing that setting restores the original behavior. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Gets Faster—and What Doesn’t
&lt;/h2&gt;

&lt;p&gt;The top of the README lists numbers for an RTX 3090 Ti. They are the median of five runs of a dense 8192³ GEMM at 450 W, with results accumulated in FP32.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Numbers in the README (reported in issue #1)&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FP16&lt;/td&gt;
&lt;td&gt;87.46 TFLOPS&lt;/td&gt;
&lt;td&gt;118.39 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BF16&lt;/td&gt;
&lt;td&gt;87.50 TFLOPS&lt;/td&gt;
&lt;td&gt;157.41 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TF32&lt;/td&gt;
&lt;td&gt;42.58 TFLOPS&lt;/td&gt;
&lt;td&gt;82.61 TFLOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These numbers weren’t measured by the repository’s author. They came from someone who posted in &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;issue #1&lt;/a&gt; and tested an experimental fork with RTX 3090 Ti support. The setup included a “modified NVIDIA 610.43.02 driver” and a custom startup script. The same issue also reports tests with the unmodified package and a standard driver. In those tests, BF16 performance rose from 63.0 to 105.9 TFLOPS. However, the power limit also changed from 300 W to 380 W, so the conditions weren’t the same before and after.&lt;/p&gt;

&lt;p&gt;A GEMM speedup doesn’t translate directly into an application speedup. The README says workloads with a lot of matrix computation get faster. It lists LLM prefill, image generation (diffusion), and FlashAttention. LLM decode is limited by memory bandwidth, so it barely changes. INT8 computation is also unchanged. One report comparing outputs found them identical down to the bit. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reports from users show a more nuanced picture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A vLLM report showed prefill getting 33–36% faster, with similar results at 8K, 32K, and 128K context lengths. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;On the same card, a GGUF engine showed no change at 8K and a 12–13% speedup at 32K and 128K. The reporter noted that gains depend on the engine and kernels, and that short prompts may see no change at all. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Two reports on an RTX 3090 showed 14–16% faster prefill at 16K, with no change at 4K. One MoE model showed no change with a very long prompt of around 32K, because loading experts from RAM took longer. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;issue #9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Decode was within the margin of error in every report. INT8 GEMM also showed no change in reports that measured it for comparison. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another factor affecting the results is the power limit. During GEMM, the card stays at its power limit, so the clock drops when it does more work per clock cycle. In one report with the limit set to 220 W, the SM clock fell from about 1,720 MHz to about 1,330–1,365 MHz. Even with roughly 1.9× more work per clock cycle, the real-world speedup doesn’t reach that level. The README also notes that its smallest gain, +25%, came from a card capped at 225 W. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;issue #15&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Cards Have Been Reported
&lt;/h2&gt;

&lt;p&gt;The compatibility table in the README summarizes reports from users. The reported BF16 GEMM gains come from different benchmarks, so they can’t be compared side by side.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Card&lt;/th&gt;
&lt;th&gt;BF16 GEMM gain (reported)&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;RTX 3090 Ti&lt;/td&gt;
&lt;td&gt;+68–80%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3090&lt;/td&gt;
&lt;td&gt;+25–67%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;#9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;#25&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;#33&lt;/a&gt; and others&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 Ti 20 GB (OEM)&lt;/td&gt;
&lt;td&gt;Some reports say it remains at half speed; others report about 2×&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/28" rel="noopener noreferrer"&gt;#28&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 10 GB&lt;/td&gt;
&lt;td&gt;+70–73%&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/14" rel="noopener noreferrer"&gt;#14&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3080 20 GB (memory-modified)&lt;/td&gt;
&lt;td&gt;+56–59%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/10" rel="noopener noreferrer"&gt;#10&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/15" rel="noopener noreferrer"&gt;#15&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3070&lt;/td&gt;
&lt;td&gt;+73–81%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/issues/10" rel="noopener noreferrer"&gt;#10&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/12" rel="noopener noreferrer"&gt;#12&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 3060 12 GB&lt;/td&gt;
&lt;td&gt;About +80%&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/Zagitta/gpu-unlock/pull/13" rel="noopener noreferrer"&gt;#13&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/31" rel="noopener noreferrer"&gt;#31&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The RTX 3070 Ti has not been tested with this module. There are no reports yet for GA103 cards.&lt;/p&gt;

&lt;p&gt;The results are mixed for the OEM RTX 3080 Ti 20 GB. In the first report, the unlock appeared to succeed and the setting’s value remained changed, but BF16 was still running at half speed. Downgrading the driver to 610.43.03 raised performance to about 65% of full speed, but it still didn’t reach full speed. Another user with two cards of the same model reported about 2× performance with driver 610.43.03. The other card, which was not unlocked, remained at its original speed. The two users’ cards have different board IDs. As of October 26, the cause is still unknown. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/28" rel="noopener noreferrer"&gt;issue #28&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Things to Consider Before Installing
&lt;/h2&gt;

&lt;p&gt;This article doesn’t cover the installation steps. Please read the README. Instead, here are the points that stood out to me after reading the primary sources.&lt;/p&gt;

&lt;p&gt;The primary sources say nothing about warranty coverage. Check with your card’s manufacturer or retailer.&lt;/p&gt;

&lt;p&gt;The boot process also changes. In the reports I read, everyone had Secure Boot disabled. Because this loads an unsigned kernel module, the kernel is marked as tainted. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/9" rel="noopener noreferrer"&gt;issue #9&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; Whether you can unlock the card again after a warm reboot depends on the board. Some users report success, while others only use it after a cold boot. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/1" rel="noopener noreferrer"&gt;issue #1&lt;/a&gt; &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; You can also use it with a card that’s driving a display. However, the screen goes blank between the reset and when &lt;code&gt;nvidia-drm&lt;/code&gt; takes over. You’ll need to unlock the card again after resuming from suspend, and hibernation isn’t supported. &lt;a href="https://github.com/Zagitta/gpu-unlock" rel="noopener noreferrer"&gt;README&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Its relationship to NVIDIA’s terms is unclear. The firmware used by the module is extracted from the NVIDIA driver package and is subject to the NVIDIA driver license agreement. The NOTICE states that permission to redistribute it publicly has not been obtained. The primary sources I read contain no record of NVIDIA commenting on this module. The mechanism itself exploits a vulnerability in NVIDIA-signed firmware. &lt;a href="https://github.com/Zagitta/gpu-unlock/blob/main/firmware/NOTICE.md" rel="noopener noreferrer"&gt;firmware/NOTICE.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Power consumption and stability are also worth considering. As noted above, gains are small when the power limit is low. Reported figures with a higher limit vary widely in their testing conditions. The longest stability report describes more than eight hours of mixed workloads without errors. &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/25" rel="noopener noreferrer"&gt;issue #25&lt;/a&gt; Another user wrote, “a few hours, not weeks.” &lt;a href="https://github.com/Zagitta/gpu-unlock/issues/33" rel="noopener noreferrer"&gt;issue #33&lt;/a&gt; We still don’t know what happens with long-term use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Measure whether prefill or decode is the source of your wait time.&lt;/li&gt;
&lt;li&gt;Compare reports with similar cards, power limits, drivers, and engines.&lt;/li&gt;
&lt;li&gt;Do not expect decode token/s to improve from this module.&lt;/li&gt;
&lt;li&gt;The README lists image generation as a workload, but the issues reviewed here provide no before and after performance numbers.&lt;/li&gt;
&lt;li&gt;Check Secure Boot requirements, kernel tainting, and the limited long-term stability reports.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/h100-only-3b-for-conversation/" rel="noopener noreferrer"&gt;We spent ¥5 million on a GPU, but only 3B parameters were usable for conversation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/compute-vs-graphics-gpu-h100-cant-render/" rel="noopener noreferrer"&gt;We rented an H100, but it couldn’t render a single frame — Compute GPUs and graphics GPUs are different&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/ollama-v1-context-4096/" rel="noopener noreferrer"&gt;Why Ollama wouldn’t call tools: a context length of 4,096&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gpu</category>
      <category>llm</category>
      <category>linux</category>
    </item>
    <item>
      <title>How to Catch Japanese Text in Automated dev.to Crossposts</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:01:18 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-catch-japanese-text-in-automated-devto-crossposts-3imo</link>
      <guid>https://dev.to/orca_forge/how-to-catch-japanese-text-in-automated-devto-crossposts-3imo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/devto-translation-shipped-in-japanese/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=devto-translation-shipped-in-japanese" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR A Japanese blog was translated and automatically posted to dev.to, but its title and description remained in Japanese. The English body and generated &lt;code&gt;devto.md&lt;/code&gt; made the post look ready, so the check for total translation failure did not stop it. A pre-post check now inspects the title, description, body, and tags, while people still review meaning and numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Body Is in English, It Slips Through
&lt;/h2&gt;

&lt;p&gt;If translation failed, no file for posting was created. That safeguard existed. But it only stopped the process when the entire translation failed and no file was produced.&lt;/p&gt;

&lt;p&gt;In a case like this one, the body was in English while only the title and description remained in Japanese. The English body had been generated, and so had &lt;code&gt;devto.md&lt;/code&gt;. From the perspective of the downstream &lt;code&gt;crosspost.mjs&lt;/code&gt;, it looked like a post-ready article was available.&lt;/p&gt;

&lt;p&gt;The rule “don’t create the file if English translation fails” assumed that failure would affect the article as a whole. In reality, each field could succeed or fail independently. The body could be translated while the title and description were left behind. There was no check to catch that partial success.&lt;/p&gt;

&lt;p&gt;It wasn’t just the title. The article also had a Japanese table and timeline inside a code block. For example, this section with the numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YouTube Shorts
  Impressions        Approx. 2,900
  Views                       279   (continued viewing: 9.5%)
  Average view duration      0:13   (about 85% of a 15.3-second video)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even when the body paragraphs had been translated into English, blocks like these weren’t necessarily in English too. Code blocks aren’t only for programs; they can also contain logs, tables of numbers, and processing timelines. Reading just some of the visible paragraphs wasn’t enough to know the state of the article as a whole.&lt;/p&gt;

&lt;h2&gt;
  
  
  The English Translation Had Other Errors, Too
&lt;/h2&gt;

&lt;p&gt;The articles I reviewed around the same time also contained mistranslations in the body text.&lt;/p&gt;

&lt;p&gt;The output token count, “134万” (1.34 million), had become &lt;code&gt;134,000&lt;/code&gt; in English. “二次関数的に膨らむ” (grow quadratically) had been translated as &lt;code&gt;exponentially&lt;/code&gt;, and “制限にはかかっていません” (it is not subject to the limit) as &lt;code&gt;it is not being used&lt;/code&gt;. Even when the meanings seem close, they do not describe the same state or value.&lt;/p&gt;

&lt;p&gt;The code block language tags were wrong, too. Every log or text block was tagged &lt;code&gt;python&lt;/code&gt;. Checking only whether the text had become English could not tell us whether the numbers were preserved, the meanings were accurate, or the block types were correct.&lt;/p&gt;

&lt;p&gt;There were limits to checking translation success with a single indicator. The fact that a file had been created did not mean that every part of the article had been translated correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural-Sounding English Errors Can’t Be Found by Checking the Characters Alone
&lt;/h2&gt;

&lt;p&gt;On October 2, 2026, a person spotted and corrected another article’s title before it was published.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Comparing Japanese &lt;strong&gt;Speech Recognition&lt;/strong&gt; with 1,000 Utterances&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The article was about classifying utterances, not speech recognition. The English phrasing wasn’t unnatural. That’s exactly why checking only for leftover Japanese characters can’t catch this kind of error. The tags also contained the same value twice: &lt;code&gt;"llm, llm"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A person caught the title before it went live. That made one thing clear: if we rely on pre-publication review, we could miss another error. We need to catch leftover Japanese, but we also need to block anything we can validate mechanically at the entry point to the publishing process.&lt;/p&gt;

&lt;p&gt;Still, character checks can’t catch every mistranslation. &lt;code&gt;Speech Recognition&lt;/code&gt; is natural English. The number &lt;code&gt;134,000&lt;/code&gt; also looks like a natural English number. Checking only the character types won’t reveal a mismatch with the original meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect the Article Itself Right Before Posting
&lt;/h2&gt;

&lt;p&gt;On October 3, 2026, I added a pre-post check to &lt;code&gt;crosspost.mjs&lt;/code&gt;. It checks the title, description, body, and tags in the actual &lt;code&gt;devto.md&lt;/code&gt; being sent—not whether the translation process succeeded.&lt;/p&gt;

&lt;p&gt;If the title or description contains hiragana or katakana, the post is held. If any Japanese remains at the point of posting, it is not sent as is. For the body, the check excludes code blocks and inline code, then holds the post if three or more lines contain kana or katakana.&lt;/p&gt;

&lt;p&gt;The body threshold is not zero. It allows one or two lines of Japanese, such as in data examples or quotations. Code blocks and inline code are also excluded from the body check. That way, Japanese characters or values there alone are not treated as a translation failure across the entire body.&lt;/p&gt;

&lt;p&gt;The check also holds the post if tags are duplicated. When a post is held, the reason is logged and a notification is sent to the Mac. If the content has been reviewed, it can still be posted with &lt;code&gt;--force&lt;/code&gt;. After automatically stopping a post, there’s still a way to review the content and let it through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results from Checking 94 Articles
&lt;/h2&gt;

&lt;p&gt;After adding the checks, I reviewed 94 existing articles. None had Japanese text left in their titles or descriptions.&lt;/p&gt;

&lt;p&gt;This means no Japanese text was found in the titles and descriptions of the 94 articles reviewed. It does not mean there were no mistranslations in the body text or that all the numbers were correct. We need to distinguish what the checks can verify from what still requires a person to read and review.&lt;/p&gt;

&lt;p&gt;This setup can catch hiragana or katakana in titles and descriptions, substantial amounts of hiragana or katakana left in the body text, and duplicate tags. It cannot identify mistranslations that read naturally in English or numbers that have been transcribed incorrectly. A person still needs to read the articles to check for those issues.&lt;/p&gt;

&lt;p&gt;This check also cannot detect text embedded in PNG diagrams, so the text in those diagrams remains in Japanese. If we create diagrams in SVG or HTML, we can replace the text and export English versions. We have decided to take that approach, but the process for creating English diagrams has not yet been implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check the title, description, body, and tags in the output immediately before posting.&lt;/li&gt;
&lt;li&gt;Hold posts when the title or description contains hiragana or katakana, or when three or more body lines contain them outside code.&lt;/li&gt;
&lt;li&gt;Hold duplicate tags, log the reason, and send a Mac notification; reviewed posts can be sent with &lt;code&gt;--force&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Human review is still needed for mistranslations and number changes that look natural in English.&lt;/li&gt;
&lt;li&gt;The check does not inspect text embedded in PNG diagrams; the English diagram workflow is not implemented yet.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devto</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>How to Recover 13 Deleted DNS Records in 11 Minutes</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Mon, 26 Oct 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/orca_forge/how-to-recover-13-deleted-dns-records-in-11-minutes-12o2</link>
      <guid>https://dev.to/orca_forge/how-to-recover-13-deleted-dns-records-in-11-minutes-12o2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/deleted-wrong-domain-dns-restored-11min/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=deleted-wrong-domain-dns-restored-11min" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR We selected a live domain beside the intended domain in the DNS management UI and deleted 13 records. AXFR was refused, but the pre-deletion list and authoritative DNS data were still available. We enumerated names, queried them with &lt;code&gt;dig&lt;/code&gt;, cross-checked public DNS caches, and manually restored the records in about 11 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;If you accidentally delete DNS records, the last known list and values retrieved from the authoritative DNS server (the server that returns the domain’s official DNS information) can help you restore them. AXFR was refused in this case, but by enumerating names, querying them with &lt;code&gt;dig&lt;/code&gt;, and cross-checking public DNS caches, I manually restored 13 records in about 11 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the DNS Records Were Before Deletion
&lt;/h2&gt;

&lt;p&gt;Thirteen records were deleted: 11 A records, one CNAME record, and one MX record. The recovery source was a complete list that another operator had copied from the management console just before deletion. They were working in a separate AI session.&lt;/p&gt;

&lt;p&gt;After deletion, it becomes difficult to remember the original values one by one. In this case, the list had been saved, so there was enough information to re-create all 13 records manually.&lt;/p&gt;

&lt;p&gt;However, the list alone could not confirm the state of the authoritative DNS. Immediately after the deletion request was submitted, the update to the authoritative DNS was delayed. Running &lt;code&gt;dig&lt;/code&gt; still returned every record that existed before deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Verified Every Record After AXFR Was Denied
&lt;/h2&gt;

&lt;p&gt;The AXFR request to transfer the entire DNS zone was denied. So I enumerated the names and queried them with &lt;code&gt;dig&lt;/code&gt;, then checked the results against a recently copied list and the records retrieved from the authoritative DNS server.&lt;/p&gt;

&lt;p&gt;I also compared them with public DNS caches—temporarily stored DNS information. I used results from 8.8.8.8 and 1.1.1.1 to double-check, and verified what was visible through those separate caches instead of relying only on the list retrieved from the authoritative DNS server.&lt;/p&gt;

&lt;p&gt;I had three sources to guide the manual restoration: the surviving list, the full set of records retrieved from the authoritative DNS server right after deletion, and the public DNS cache results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying Recovery Time Using TTL and SOA Serial
&lt;/h2&gt;

&lt;p&gt;The A record’s TTL (the length of time DNS information is kept in cache) was 3,600 seconds, and the negative cache TTL (how long a “does not exist” response is cached) was 300 seconds. The SOA serial (the zone’s update number) showed that the zone became empty at 10:06 and was restored by re-registration at 10:17. According to the records, recovery took about 11 minutes.&lt;/p&gt;

&lt;p&gt;Recovery depended on more than the TTL values. Even after deletion, we could retrieve all records from the authoritative DNS server and compare them with the previous list and the results from public DNS caches. This let us verify what to enter manually.&lt;/p&gt;

&lt;p&gt;We didn’t undo the deletion in the admin console. Instead, we gathered information to verify the records and re-registered them manually. Because the original values were still available, we could proceed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Avoid Accidentally Deleting DNS Records
&lt;/h2&gt;

&lt;p&gt;This mistake happened while I was working with domains that had similar names on the same screen. I meant to delete an unused domain, but selected the adjacent one that was still active. The lesson: don’t manage similarly named domains on the same screen, and export the entire zone before deleting anything.&lt;/p&gt;

&lt;p&gt;Even when you try to retrieve every record in a zone with AXFR, the request may be denied. That happened here, so I enumerated the names and queried them with &lt;code&gt;dig&lt;/code&gt;. Keeping a copy of the list before deleting records means you won’t have to start from scratch searching for their values afterward.&lt;/p&gt;

&lt;p&gt;I restored everything in 11 minutes, not because I found a special recovery feature, but because I had a recently copied list, could retrieve every record with &lt;code&gt;dig&lt;/code&gt; while the changes were still propagating to the authoritative DNS servers, and could cross-check the results against public DNS caches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check for a pre-deletion record list in the management UI.&lt;/li&gt;
&lt;li&gt;If authoritative DNS propagation is delayed, enumerate names and use &lt;code&gt;dig&lt;/code&gt; to see whether all records can be retrieved.&lt;/li&gt;
&lt;li&gt;Cross-check results against the public DNS caches at 8.8.8.8 and 1.1.1.1.&lt;/li&gt;
&lt;li&gt;Manually re-register the verified values and check the time with the SOA serial.&lt;/li&gt;
&lt;li&gt;Before the next deletion, identify the target domain and export the full zone.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dns</category>
    </item>
    <item>
      <title>Why MediaRecorder Turns a 190-Second Recording Into 92 Seconds</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sun, 25 Oct 2026 13:19:57 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-mediarecorder-turns-a-190-second-recording-into-92-seconds-4jpf</link>
      <guid>https://dev.to/orca_forge/why-mediarecorder-turns-a-190-second-recording-into-92-seconds-4jpf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/mediarecorder-recording-half-lost/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=mediarecorder-recording-half-lost" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR MediaRecorder delivers recording data in chunks, and processing them is asynchronous. If stopping closes the file before pending chunk sends finish, the final data is lost. Silent audio and a stop check that misses the studio avatar's voice can also cause empty files or cut speech.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;The reason a roughly 190-second recording with MediaRecorder lasted only 92 seconds was that the system didn't wait for the final chunk processing upon stopping, no audio data was sent during silence, and the stop logic did not account for the end of the studio-side audio. I implemented fixes to wait for the chunk transmission to complete, stream silence using a &lt;code&gt;ConstantSource&lt;/code&gt;, and ensure the recording continues until the studio-side audio track has actually finished. After these fixes, a 60-second recording of silence was saved correctly with 60.49 seconds of video and 60.48 seconds of audio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the last data was missing after stopping the recording
&lt;/h2&gt;

&lt;p&gt;The reason for the missing last data was that the main side was notified of the recording stop before finishing processing the chunks.&lt;/p&gt;

&lt;p&gt;MediaRecorder divides the recorded data into chunks and passes them on. Even after receiving a chunk, the process of converting its contents to a byte array is asynchronous (a method that proceeds to the next process without waiting for completion). Right before stopping, the last chunk had arrived, but I didn't wait for the processing to finish and notified the main side to stop the recording.&lt;/p&gt;

&lt;p&gt;The main side receives the stop instruction and closes the file. Even if the chunk processing is completed afterwards, the destination for writing is already closed. Although it seems like the recording was working until the end, the last data is not saved.&lt;/p&gt;

&lt;p&gt;In the fix, I processed the chunks in the order they were received and sent the next one after the previous chunk's transmission was completed. When stopping, I wait until all pending transmissions are finished. What was necessary was not the fact that the stop was called, but that the processing started before the stop was completed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why My 60-Second Voiceless Recording Resulted in a 0-Byte File
&lt;/h2&gt;

&lt;p&gt;The reason the voiceless recording came out as a 0-byte file was that no data was flowing through the audio channel.&lt;/p&gt;

&lt;p&gt;When I examined the case where I recorded for 60 seconds without any voice, I found that no audio data was being transmitted through the audio track used for the recording until a voice actually entered. With the audio input empty, the &lt;code&gt;MediaRecorder&lt;/code&gt; did not write anything, resulting in a 0-byte file even after stopping the recording.&lt;/p&gt;

&lt;p&gt;I had assumed that only the video was being captured. In reality, the recording input included an audio track (the audio data intended to be included in the recording), and this audio channel remained empty during periods of silence.&lt;/p&gt;

&lt;p&gt;I connected a &lt;code&gt;ConstantSource&lt;/code&gt; (an audio source that continuously outputs a constant value) to the audio channel, outputting a value of 0. This ensures that silent audio data is always flowing. The purpose is to continue passing the audio track to the recording even during moments when no voice is present. I discussed this mechanism in a previous article. What I discovered this time is that waiting for 60 seconds without any voice resulted in a 0-byte file, and that the root cause was the lack of incoming data on the audio channel. Even if the intent is to record video only, when using a recording setup that combines an audio track, the state of the audio input can significantly affect the final output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the last voice of the studio-side avatar was cut off
&lt;/h2&gt;

&lt;p&gt;The reason why the last word was cut off in the middle is that the recording stop judgment was not looking at the voice of the studio-side avatar.&lt;/p&gt;

&lt;p&gt;The stop judgment was looking at the voice sounding in the avatar window on the desktop. However, the voice of the avatar that appears only on the broadcast screen is on the studio side. The window being looked at by the stop judgment does not have the end time of that voice.&lt;/p&gt;

&lt;p&gt;I thought that if the side that determines the stop of the recording judges that "it's quiet now", it would also think that the voice on the studio side has ended. However, that premise was incorrect. Even just before stopping the recording, there were cases where the voice on the stage was still sounding. In fact, in a scene where the last word lasted for about 34 seconds, it was cut off after about 2 seconds.&lt;/p&gt;

&lt;p&gt;So, I made the studio side have the end time of the voice for each avatar. When stopping the recording, it waits until the time when the last voice ends plus 1.5 seconds. However, this waiting time has an upper limit of 120 seconds. I also extended the main side's save wait time from 15 seconds to 150 seconds. This is to prevent the main side's wait from being cut off first while the studio is waiting for the end of the voice.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Recording Time and Audio Length Changed After Fixes
&lt;/h2&gt;

&lt;p&gt;After the fixes, the video and audio lengths were almost aligned, and the final subtitles fit within the video.&lt;/p&gt;

&lt;p&gt;Recording 60 seconds without speaking resulted in a video length of 60.49 seconds and an audio length of 60.48 seconds. A recording with six dialogue exchanges resulted in a video length of 171.42 seconds and an audio length of 171.37 seconds.&lt;/p&gt;

&lt;p&gt;The alignment was achieved by revisiting the processing at three points before stopping the recording: waiting for chunk transmission, streaming silence in the audio path during silent periods, and waiting for the avatar's voice to end in the studio before deciding to stop. Fixing only one of these would still result in gaps in the remaining paths.&lt;/p&gt;

&lt;p&gt;The recording time and the saved video length are not always the same. It was necessary to track not only the time when the stop was called but also the ongoing processes before and after, as well as which audio the termination judgment was monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Process chunks in order and wait for all pending sends before stopping.&lt;/li&gt;
&lt;li&gt;Feed silent data into the audio path with ConstantSource when no one is speaking.&lt;/li&gt;
&lt;li&gt;Use the studio avatar's voice end time in the stop check.&lt;/li&gt;
&lt;li&gt;Cap the studio wait at 120 seconds and extend the main save wait from 15 to 150 seconds.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mediarecorder</category>
      <category>webaudioapi</category>
    </item>
    <item>
      <title>Why a VRM Right Hand Disappeared: Empty Cuff Ring and NaN</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:04:39 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-a-vrm-right-hand-disappeared-empty-cuff-ring-and-nan-2f47</link>
      <guid>https://dev.to/orca_forge/why-a-vrm-right-hand-disappeared-empty-cuff-ring-and-nan-2f47</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/vrm-right-hand-vanished-nan/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=vrm-right-hand-vanished-nan" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR A VRM export made only the right hand disappear, even across edits and poses. The shared cuff position found no right-hand vertices, leaving an empty ring. Its mean produced NaN values that spread to the pivot and finger bones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;The disappearance of the right hand in the VRM was caused by the right hand's vertices failing to reach the common cuff position for both arms, which allowed NaN (Not a Number) values to propagate from an empty loop. We fixed this by ensuring that scaling transforms are not baked into the vertices, and by adding a safeguard that halts the process if NaN values are detected in empty loops or bones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Was the Disappearing Right Hand a Display or Pose Issue?
&lt;/h2&gt;

&lt;p&gt;On October 5 at 17:01 UTC, a user reported an issue: only the right hand was missing on screen, both in an unedited state and regardless of the pose. My first thought was that it was a visibility toggle or posing issue.&lt;/p&gt;

&lt;p&gt;When I inspected the VRM exported at 17:11 UTC, the Hands mesh had 1,462 vertices. Of those, 711 vertices—accounting for the entire right hand—had their positions set to NaN. The renderer simply doesn't draw vertices with NaN coordinates. The invisible hand wasn't a rendering bug after all.&lt;/p&gt;

&lt;p&gt;Initially, I checked component visibility toggles, edit operations, weight sources, poses, and culling. (Weights define how much a bone influences vertices.) The hand's scale was also set to 1. None of these were the culprit.&lt;/p&gt;

&lt;h2&gt;
  
  
  At which stage was NaN born?
&lt;/h2&gt;

&lt;p&gt;I investigated the pipeline—the processing flow up until export—stage by stage. The files were normal up until the manual adjustment stage. Starting from the files in the synchronization stage, the rest matrix (a matrix representing the reference pose of the bones) and lengths for the 15 bones of the right finger were NaN.&lt;/p&gt;

&lt;p&gt;NaN did not suddenly appear during export. Even before the synchronization stage, the input for the coordinate generation process was already corrupted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Ring at the Cuff's Vertex Became Empty
&lt;/h2&gt;

&lt;p&gt;The data before modification was normal. The change began with the influence of the 3D editing done afterwards.&lt;/p&gt;

&lt;p&gt;In the modification, a shrink transformation was attached to the Hands object. In world coordinates (coordinates based on the entire scene), it changed from -0.586 to +0.662, and the right hand was shifted about 7cm inward.&lt;/p&gt;

&lt;p&gt;After that, step 1 of replacing the head bakes the transformation of all objects into the vertices. Baking refers to the process of reflecting the transformation attached to an object into the vertex coordinates. Although step 2 of synchronization also has a process called &lt;code&gt;matrix_world = Identity&lt;/code&gt;, it is after baking, so the transformation cannot be restored here.&lt;/p&gt;

&lt;p&gt;The problematic calculation was in step 6. The cuff position was determined by a common value for both left and right sides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cuff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;  &lt;span class="c1"&gt;# approximately 0.63
&lt;/span&gt;&lt;span class="n"&gt;x_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;approximately&lt;/span&gt; &lt;span class="mf"&gt;0.61&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right hand only reaches up to -0.586. Even if we search for vertices on the cuff side from approximately 0.61, there are no corresponding vertices for the right hand. The ring (a collection of vertices along the cuff) on the right became empty.&lt;/p&gt;

&lt;p&gt;Taking the average of an empty ring results in NaN. The pivot (the reference point for transformation) becomes NaN from that value, and NaN spreads to the 15 bones of the right fingers as well. The direct cause of the destruction of 711 vertices was handling different ranges on the left and right with a common cuff value.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Stop the Empty Loop and NaN in Bones
&lt;/h2&gt;

&lt;p&gt;The correction was done in two stages.&lt;/p&gt;

&lt;p&gt;In the stage of replacing the head, the hands are excluded from the conversion target. The reduction that was attached to the hands was prevented from being baked into the vertices.&lt;/p&gt;

&lt;p&gt;In the synchronization stage, a process was added to raise a &lt;code&gt;RuntimeError&lt;/code&gt; if the loop is empty. Furthermore, if there is an NaN in the bones, it is stopped by an assert (a check that confirms the condition and stops the process if it is not met). This is to detect abnormal values before proceeding and exporting VRM.&lt;/p&gt;

&lt;p&gt;Re-execution took real 0m21.657s and exited with 0. The log output included "hands ×0.924 (from sleeve to fingertips 0.134 m)". In the confirmation output, the number of NaNs in the 1462 vertices of the Hands mesh was 0.&lt;/p&gt;

&lt;p&gt;It was confirmed that the hands were restored. However, this alone did not reveal other problems in the processing system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Python Errors Don’t Stop Execution in Our Pipeline
&lt;/h2&gt;

&lt;p&gt;In a related log from another day, a 3D editing application was returning exit code 0 despite encountering Python errors. Relying solely on the exit code meant the calling process couldn’t detect the anomaly.&lt;/p&gt;

&lt;p&gt;On 10-06, I added &lt;code&gt;--python-exit-code 1&lt;/code&gt; to some steps. By 10-07, all steps were routed through a common function, using both &lt;code&gt;--python-exit-code 1&lt;/code&gt; and &lt;code&gt;set -e -o pipefail&lt;/code&gt;. The &lt;code&gt;grep&lt;/code&gt; command for log verification was wrapped in &lt;code&gt;{ grep … || true; }&lt;/code&gt; to prevent the entire process from halting if no results were found.&lt;/p&gt;

&lt;p&gt;Additionally, I introduced a &lt;code&gt;FAIL_AT=&amp;lt;step&amp;gt;&lt;/code&gt; option for testing. Setting &lt;code&gt;FAIL_AT=ring&lt;/code&gt; intentionally triggered a failure, confirming that the process stopped with exit code 1 and subsequent steps were skipped.&lt;/p&gt;

&lt;p&gt;This exit code issue is separate from the problem of the missing right hand. However, if a pipeline detects an anomaly but continues processing, corrupted values will propagate downstream. It’s essential not only to log errors but also to ensure the process halts at the correct point when anomalies occur.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Verify Values Are Created at the Right Stage
&lt;/h2&gt;

&lt;p&gt;Simply chasing NaN vertices didn't lead to the root cause of the disappearing right hand. It was only after comparing each stage in order—applying transforms, baking transforms, and defining the cuffs—that I finally tracked it down to an empty loop.&lt;/p&gt;

&lt;p&gt;When processing values shared between the left and right sides, you can't assume both sides will have the same range of data. To fix this, I made sure the calculation aborts if the loop is empty and added validation for the bone values as well.&lt;/p&gt;

&lt;p&gt;Finally, I intentionally induced failures to confirm whether the pipeline truly stops when a step returns an error. The fix wasn't complete just by bringing the right hand back; it was about ensuring we never miss an anomaly the next time one occurs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/video-to-vrm-pipeline-overview/" rel="noopener noreferrer"&gt;Making a VRM Dance from a Single Live-Action Video&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://forge.workstyle.tech/blog/wrist-crease-is-skinning-not-pose/" rel="noopener noreferrer"&gt;The Cause of Wrist Skin Creasing Was Skinning, Not the Pose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check whether the issue affects only the right hand and reproduces across edits and poses.&lt;/li&gt;
&lt;li&gt;Check the exported VRM Hands mesh for NaN values.&lt;/li&gt;
&lt;li&gt;Compare files before hand editing, after transform baking, and at the sync stage.&lt;/li&gt;
&lt;li&gt;Stop when the cuff ring is empty, and check bones for NaN values.&lt;/li&gt;
&lt;li&gt;Make Python errors return exit code 1 and confirm later pipeline steps do not run.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vrm</category>
      <category>blender</category>
      <category>python</category>
      <category>3d</category>
    </item>
    <item>
      <title>Why uvx MCP servers fail with Pydantic 2.14</title>
      <dc:creator>orca forge</dc:creator>
      <pubDate>Fri, 23 Oct 2026 13:07:17 +0000</pubDate>
      <link>https://dev.to/orca_forge/why-uvx-mcp-servers-fail-with-pydantic-214-i77</link>
      <guid>https://dev.to/orca_forge/why-uvx-mcp-servers-fail-with-pydantic-214-i77</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;📝 Originally published (in Japanese) at &lt;a href="https://forge.workstyle.tech/blog/pydantic-214-breaks-old-mcp-servers/?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=pydantic-214-breaks-old-mcp-servers" rel="noopener noreferrer"&gt;forge.workstyle.tech&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;TL;DR: Older &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; servers load Pydantic's internal &lt;code&gt;eval_type_backport&lt;/code&gt; function.&lt;br&gt;
Pydantic 2.14 no longer has that function, so startup fails with an &lt;code&gt;ImportError&lt;/code&gt;.&lt;br&gt;
Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; to the applicable &lt;code&gt;uvx&lt;/code&gt; configuration, then check each server's &lt;code&gt;initialize&lt;/code&gt; response after reconnecting.&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion First
&lt;/h2&gt;

&lt;p&gt;Old &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; MCP servers started with &lt;code&gt;uvx&lt;/code&gt; fail to launch due to &lt;code&gt;ImportError: cannot import name 'eval_type_backport'&lt;/code&gt; with Pydantic 2.14. Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; to pin Pydantic below 2.14, then reconnect and verify the per-server &lt;code&gt;initialize&lt;/code&gt; response.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Pydantic 2.14 Causes an ImportError
&lt;/h2&gt;

&lt;p&gt;The target server was pinned to &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;, which belongs to an older &lt;code&gt;mcp&lt;/code&gt; release line. The constraint &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; specifies using a version lower than 1.10. This version line imports an internal function called &lt;code&gt;eval_type_backport&lt;/code&gt; from Pydantic. However, that function was removed in Pydantic 2.14.&lt;/p&gt;

&lt;p&gt;As a result, the server fails with an &lt;code&gt;ImportError&lt;/code&gt; when attempting to import the missing function. Both the SQLite and Hacker News configurations where this error occurred were being launched via &lt;code&gt;uvx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The workaround is to pin Pydantic to below 2.14 in addition to the older &lt;code&gt;mcp&lt;/code&gt; version. Add the following flags to your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--with "mcp&amp;lt;1.10" --with "pydantic&amp;lt;2.14"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The new addition here is &lt;code&gt;pydantic&amp;lt;2.14&lt;/code&gt;. The &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt; constraint was already in place to maintain compatibility with older servers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Pydantic &amp;lt;2.14 Specification to uvx Arguments
&lt;/h2&gt;

&lt;p&gt;If you're separating commands and arguments in configuration screens, add &lt;code&gt;--with&lt;/code&gt; and the package specification to the &lt;code&gt;uvx&lt;/code&gt; arguments. For a SQLite server, for example, it would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;command: uvx
args: ["--with", "mcp&amp;lt;1.10", "--with", "pydantic&amp;lt;2.14", "mcp-server-sqlite", "--db-path", "&amp;lt;SQLite DB path&amp;gt;"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;&amp;lt;SQLite DB path&amp;gt;&lt;/code&gt; with the location of your existing database. In the startup arguments, pass this value after &lt;code&gt;--db-path&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Apply the same fix to the Hacker News server. The package name to start is &lt;code&gt;mcp-hn&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;command: uvx
args: ["--with", "mcp&amp;lt;1.10", "--with", "pydantic&amp;lt;2.14", "mcp-hn"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;--with "mcp&amp;lt;1.10"&lt;/code&gt; is already present, leave that specification as is and add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt;. Review the entire configuration to ensure there are no duplicate Pydantic version pins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify MCP Server &lt;code&gt;initialize&lt;/code&gt; Response After Reconnection
&lt;/h2&gt;

&lt;p&gt;After saving your configuration, reconnect to the target MCP server. A simple "connected" status doesn't confirm that the initialization request (&lt;code&gt;initialize&lt;/code&gt;) was successful. You'll need to verify the response on the client side.&lt;/p&gt;

&lt;p&gt;Check SQLite and Hacker News separately. Even if one initializes successfully, it doesn't guarantee the other has as well. If errors disappear and both respond to initialization, you've avoided the startup &lt;code&gt;ImportError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you want to verify using only locally available dependencies, you can use &lt;code&gt;uvx --offline&lt;/code&gt;. Our records show that both SQLite and Hacker News respond to MCP initialization offline. However, this method may not work in environments without locally installed packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Existing Configurations to Add Pydantic Specifications To
&lt;/h2&gt;

&lt;p&gt;Some older configurations only pinned &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;. In those cases, add the Pydantic specification. However, for configurations where users have already specified their own Pydantic version, do not alter that value.&lt;/p&gt;

&lt;p&gt;The target for modification is configurations that start with &lt;code&gt;uvx&lt;/code&gt;, include &lt;code&gt;--with mcp&amp;lt;1.10&lt;/code&gt;, and do not yet have a Pydantic specification. Only add it to servers that meet these conditions. Do not mechanically apply the same change to configurations with different startup commands or those that already specify Pydantic.&lt;/p&gt;

&lt;p&gt;This error was related to compatibility between dependency packages rather than the server's startup command itself. When &lt;code&gt;eval_type_backport&lt;/code&gt; is not found, check if an older version of &lt;code&gt;mcp&lt;/code&gt; is being used. If so, pin Pydantic to below 2.14 and verify the &lt;code&gt;initialize&lt;/code&gt; response for each server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Check that the affected server starts with &lt;code&gt;uvx&lt;/code&gt; and specifies &lt;code&gt;mcp&amp;lt;1.10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;--with "pydantic&amp;lt;2.14"&lt;/code&gt; only when the configuration has no Pydantic version specified.&lt;/li&gt;
&lt;li&gt;Reconnect and check the &lt;code&gt;initialize&lt;/code&gt; response for SQLite and Hacker News separately.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;uvx --offline&lt;/code&gt; to check with local dependencies when available.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>mcp</category>
      <category>uvx</category>
      <category>pydantic</category>
      <category>python</category>
    </item>
  </channel>
</rss>
