<?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: Peter Mamrukov</title>
    <description>The latest articles on DEV Community by Peter Mamrukov (@netbka).</description>
    <link>https://dev.to/netbka</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%2F4191761%2Fe1fe8cb6-436b-4da8-b138-1c345088fea4.jpg</url>
      <title>DEV Community: Peter Mamrukov</title>
      <link>https://dev.to/netbka</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/netbka"/>
    <language>en</language>
    <item>
      <title>Start a long job on your PC and check it from your phone</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Wed, 28 Oct 2026 16:02:56 +0000</pubDate>
      <link>https://dev.to/netbka/start-a-long-job-on-your-pc-and-check-it-from-your-phone-1iid</link>
      <guid>https://dev.to/netbka/start-a-long-job-on-your-pc-and-check-it-from-your-phone-1iid</guid>
      <description>&lt;p&gt;Some jobs take an hour: the full test suite, a video export, copying a photo archive to a new disk, re-indexing a database. You start it, and then you want to leave. The question in every thread about this is the same: how do I see how it's going without walking back to that screen?&lt;/p&gt;

&lt;p&gt;There are three good answers, and they suit different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. tmux plus SSH: the terminal, from anywhere
&lt;/h2&gt;

&lt;p&gt;Start the job inside &lt;code&gt;tmux&lt;/code&gt; (or &lt;code&gt;screen&lt;/code&gt;) on the machine. Reach the machine over Tailscale or your VPN, SSH in from your phone with an app like Termius or Blink, and run &lt;code&gt;tmux attach&lt;/code&gt;. You see exactly what you'd see at the desk.&lt;/p&gt;

&lt;p&gt;Good for: anything you started by hand in a terminal, and anyone comfortable with a terminal on a phone keyboard.&lt;br&gt;
Watch out for: the job dies if you started it outside tmux and the terminal closes. And a 6-inch screen full of compiler output isn't a summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Claude Code's Remote Control: the same coding session, on another device
&lt;/h2&gt;

&lt;p&gt;If the long job is a Claude Code session, Claude Code has this built in. Run &lt;code&gt;claude remote-control&lt;/code&gt; on the machine (or type &lt;code&gt;/remote-control&lt;/code&gt; inside a session) and pick the session up from claude.ai/code or the Claude app. The work keeps running on your computer; the phone is a second window onto the same conversation. It needs a paid Claude plan and a claude.ai login.&lt;/p&gt;

&lt;p&gt;Good for: an interactive coding session you want to steer from the couch.&lt;br&gt;
Watch out for: it's one session on one machine. It doesn't help with a backup script or a job on a second computer.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Ask your AI chat to start it, then ask again later
&lt;/h2&gt;

&lt;p&gt;The third pattern starts the job from the chat itself. You ask ChatGPT or Claude to run the job on a named computer; the job keeps running on that computer after the chat answers; later you come back to the same conversation and ask how it's going. Your chats sync between desktop and phone, so "later" can be from the train.&lt;/p&gt;

&lt;p&gt;I build Local Commander, which works this way, so I'm biased. A real exchange looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Home PC, copy D:\Photos to the new disk E:\Photos with robocopy, keeping timestamps. Log to E:\copy.log. Tell me when it's started; don't delete anything on D.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The command starts and the chat answers within seconds with "started". An hour later, from the phone, in the same chat:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How's the copy going? Show me the last lines of the log and whether it finished with errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI asks the computer for the job's status and output and gives you a one-paragraph answer instead of a wall of log. If something is wrong: "Stop the copy." It sends the cancel to that computer.&lt;/p&gt;

&lt;p&gt;What makes "ask again later" work is that the job has an identity. When the chat starts it, the computer hands back an operation ID, and the job is written to a small journal on that computer. Later questions ("how's it going", "show me the log", "stop it") refer to that ID, so the answer is about the job you started, not a fresh guess. If the connection between the chat and the computer drops for a moment, the job keeps running where it is; the result waits in the journal until the chat asks for it.&lt;/p&gt;

&lt;p&gt;The real one I checked from the phone this week was not a photo copy. I ship the Local Commander website itself this way: from the chat I start the release script on the server, it rebuilds the site image and runs its checks for a minute or two, and I ask for the result from wherever I am.&lt;/p&gt;

&lt;p&gt;Good for: jobs that aren't a coding session — copies, backups, test runs, exports — and machines other than the one in front of you.&lt;br&gt;
Watch out for: it reports output and status, not the screen. And as with the other two, the computer has to stay awake.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one when
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Something you started by hand in a terminal&lt;/td&gt;
&lt;td&gt;tmux + SSH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A Claude Code session you want to keep steering&lt;/td&gt;
&lt;td&gt;Remote Control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A copy, backup, export or test run you'd rather describe than type&lt;/td&gt;
&lt;td&gt;Ask the chat to start it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several jobs on several computers&lt;/td&gt;
&lt;td&gt;Ask the chat; name each computer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The part everyone forgets: sleep, offline, restart
&lt;/h2&gt;

&lt;p&gt;Whatever you pick, a laptop that sleeps when you leave stops the job. On Windows, set "When plugged in, put my device to sleep after" to Never for the duration, or run &lt;code&gt;powercfg /change standby-timeout-ac 0&lt;/code&gt; and set it back later. On macOS, &lt;code&gt;caffeinate -i&lt;/code&gt; in front of the command keeps the Mac awake until it finishes. On Linux, &lt;code&gt;systemd-inhibit&lt;/code&gt; does the same.&lt;/p&gt;

&lt;p&gt;Three different things can happen while you are away, and it helps to know which one you got:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline for a while&lt;/strong&gt; (Wi-Fi hiccup, the relay restarts): the job keeps running on the computer. When the connection is back, "how's it going" gets the real status from the journal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asleep&lt;/strong&gt;: the job is paused with the computer. It resumes when the computer wakes, but a copy with a timeout or a test with a clock may have failed by then. The status tells you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restarted&lt;/strong&gt; (power cut, Windows Update): the job is gone. Nothing here restarts it for you, and it is not re-run on a guess; the chat reports the outcome as unknown, and you decide whether to start it again. Check the log before you do, so a half-finished copy is not copied twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up the chat way
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;On the computer, run &lt;code&gt;npx local-commander-agent@latest&lt;/code&gt; (Node.js 20+) and approve it in the browser.&lt;/li&gt;
&lt;li&gt;Choose its access: the whole user account, or only the folders the job needs.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;https://localcmd.app/mcp&lt;/code&gt; as a connector in ChatGPT or Claude and sign in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It isn't a sandbox: an approved computer lets the AI do what your account can do, so for a long unattended job, give it only the folders it needs. You can cancel the job or cut the computer off at any time.&lt;/p&gt;

&lt;p&gt;Steps and examples: &lt;a href="https://localcmd.app/control-multiple-computers-with-ai" rel="noopener noreferrer"&gt;https://localcmd.app/control-multiple-computers-with-ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://localcmd.app/use-cases/check-a-long-job-from-your-phone" rel="noopener noreferrer"&gt;localcmd.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>ai</category>
      <category>claude</category>
      <category>devops</category>
    </item>
    <item>
      <title>My PC started lagging once AI agents moved in. Two causes, both fixable by hand</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Mon, 26 Oct 2026 19:42:48 +0000</pubDate>
      <link>https://dev.to/netbka/my-pc-started-lagging-once-ai-agents-moved-in-two-causes-both-fixable-by-hand-2170</link>
      <guid>https://dev.to/netbka/my-pc-started-lagging-once-ai-agents-moved-in-two-causes-both-fixable-by-hand-2170</guid>
      <description>&lt;p&gt;At some point my home PC started to lag. Not crash, just hesitate: a window takes a second to open, the cursor freezes for a beat. That machine runs AI coding agents almost around the clock, Claude Code and Codex, editing code, installing packages, running builds and tests.&lt;/p&gt;

&lt;p&gt;I didn't go digging myself. I asked one line in a chat: "Why is my home PC lagging? Look at what's loading the CPU and disk and name the main cause. Don't change anything." The chat reached the computer, looked at processes and disks, and came back with two causes. I would have spent a long time finding either of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause one: Defender scans every file the agents write
&lt;/h2&gt;

&lt;p&gt;Windows' built-in antivirus checks every created and modified file in real time. A person changes a few dozen files a day. An agent changes tens of thousands: package installs, builds, a separate copy of the project for every task. Defender opens and scans each one. In Task Manager it shows up as "Antimalware Service Executable" eating CPU while you seem to be doing nothing.&lt;/p&gt;

&lt;p&gt;How to check by hand. In an elevated PowerShell, record Defender's work for a couple of minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-MpPerformanceRecording&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RecordTo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Temp\defender.etl&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# let your agents work for a couple of minutes, then press Enter&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-MpPerformanceReport&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\Temp\defender.etl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TopProcesses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TopExtensions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-TopFiles&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The report shows whose files took Defender the longest. If node, git and your project folders are at the top, that's your answer.&lt;/p&gt;

&lt;p&gt;Two ways to deal with it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exclude the agents' working folders from scanning. The price is clear: Defender won't catch anything in those folders any more. Agents download packages from the internet, so exclude only code folders, not Downloads and not a whole drive.&lt;/li&gt;
&lt;li&gt;Windows 11 has a Dev Drive, a separate volume for development work. Defender scans it in a lighter mode that doesn't hold up writes. It's a middle ground between "scan everything" and "scan nothing".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We took the first route and went a bit further. We excluded the working folders where agents create files, and the agent programs themselves: Claude Code, Codex, Antigravity, Cursor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionPath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\Work"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Add-MpPreference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ExclusionProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\path\to\agent.exe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excluding a program is a bigger step than excluding a folder. Defender stops scanning the files that program opens and writes, in any folder. Agents pull other people's packages from the internet, so I took that risk on purpose: it's a work machine, and the lag was costing me more. If that's not true for you, start with folders only, or with a Dev Drive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cause two: the agents wrote to drive C even though they were told not to
&lt;/h2&gt;

&lt;p&gt;Every one of my agents has the same line in its instructions: work on drive D only. The projects really are on D. And yet the system drive C kept filling up.&lt;/p&gt;

&lt;p&gt;It turned out it wasn't the agents writing there, it was the programs they run, and the agent apps themselves: package caches, app data, temp files. By default all of that lands in the user's AppData on drive C. The agent honestly works in D:\, and the gigabytes pile up in C:\Users\…\AppData.&lt;/p&gt;

&lt;p&gt;This is the part worth remembering: an instruction controls what the agent does, not where the programs it launches write.&lt;/p&gt;

&lt;p&gt;How to see which AppData folders are the biggest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;APPDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Folder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;Round&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On my machine the top entries were the npm cache (including the npx cache, which lives inside it) and Docker's data. Agents run all sorts of things through npx, and every version stays in the cache. We cleaned up like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--force&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c"&gt;# npm cache, including packages fetched by npx&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;df&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="c"&gt;# how much images, containers and build cache take&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;docker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;prune&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="c"&gt;# remove stopped containers and anything unused&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cleaning treats the symptom: in a month the caches grow back. So we also moved the folders themselves to D and left a link on C pointing to the new place. Programs still write "to AppData", while the files physically live on D. Close the program that owns the folder first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;robocopy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\SomeTool"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\AppData\SomeTool"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/E&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/COPY:DAT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Rename-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\SomeTool"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SomeTool.old"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mklink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;/J&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\SomeTool"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D:\AppData\SomeTool"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# check the program still works, and only then delete SomeTool.old&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some programs offer a cleaner route: tell them to keep their cache elsewhere. For npm, for example: &lt;code&gt;npm config set cache D:\npm-cache&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;CPU load dropped from 67% to about 12%. No new hardware, no reinstall: Defender simply stopped re-scanning files we were creating ourselves. The PC stopped lagging, and I went back to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A computer running agents is a different kind of load. It writes files hundreds of times more often, and anything that inspects files starts to drag.&lt;/li&gt;
&lt;li&gt;Checking what the agent does isn't enough. Check where the traces of its work end up.&lt;/li&gt;
&lt;li&gt;You can hand the diagnosis to an AI in one question. Just add "don't change anything" and ask what the conclusion is based on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I asked through &lt;a href="https://localcmd.app" rel="noopener noreferrer"&gt;Local Commander&lt;/a&gt;, my own project, which lets a chat reach my computers. But both checks above take half an hour by hand without it.&lt;/p&gt;

&lt;p&gt;Have your agents ever slowed a machine down in a way you had to hunt for?&lt;/p&gt;

</description>
      <category>windows</category>
      <category>ai</category>
      <category>productivity</category>
      <category>powershell</category>
    </item>
    <item>
      <title>We reverse-engineered a low-code platform's "Check" button and put its widget code in git</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:42:25 +0000</pubDate>
      <link>https://dev.to/netbka/we-reverse-engineered-a-low-code-platforms-check-button-and-put-its-widget-code-in-git-2ea9</link>
      <guid>https://dev.to/netbka/we-reverse-engineered-a-low-code-platforms-check-button-and-put-its-widget-code-in-git-2ea9</guid>
      <description>&lt;p&gt;Low-code platforms promise you will barely write code. That holds for a few months. Then you have dozens of forms, several people editing them, and someone who "just tweaked a script in production". It turns out you are doing software development after all, only without git, without review and without a way to roll back.&lt;/p&gt;

&lt;p&gt;I lead a business applications team in a holding company. We build modules for contracts, counterparties, correspondence and powers of attorney on ELMA 365, a low-code BPM platform popular in Russian-speaking markets. Part of the code is written by AI coding agents. This post is about the pipeline we built to get widget code out of the browser and into a repository, and the traps we found on the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The starting point
&lt;/h3&gt;

&lt;p&gt;In ELMA 365, almost everything is configured in a web designer: forms, fields, processes, permissions. Widgets carry TypeScript scripts. Client scripts run in the user's browser, server scripts run on the platform. You write them in a code editor inside the designer, press "Check", press "Publish", done.&lt;/p&gt;

&lt;p&gt;That works for one developer. For a team where AI agents write code, it is a dead end. An agent needs a file. A reviewer needs a diff. A manager needs to know what changed on Tuesday.&lt;/p&gt;

&lt;p&gt;The platform ships a CLI that exports a solution to files. Export works well: hundreds of files, forms, processes, permissions and scripts, all ready for git. Import is the problem. Label and form changes went back fine. As soon as a widget script changed, the import failed. We reproduced it on several platform builds. The official position is clear: widget scripts are written in the designer, and there is no documented path around it.&lt;/p&gt;

&lt;p&gt;We could have accepted that. Instead we went to see what the button actually does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stopgap: a robot in the designer
&lt;/h3&gt;

&lt;p&gt;While we investigated, work had to ship. So the first solution was an honest hack: a headless browser that logs in, opens the widget, pastes the code, and clicks Save, Check and Publish.&lt;/p&gt;

&lt;p&gt;Every step cost an evening. The code editor auto-closes brackets, so typing code character by character produces garbage. You paste through the clipboard instead. You cannot verify the paste by reading the hidden textarea either, because it only mirrors the visible part of the file.&lt;/p&gt;

&lt;p&gt;The worst surprise came after a platform upgrade. In the new UI, the Publish button stayed enabled even when Check had reported compile errors. A robot that looked at button state could have published broken code. We changed it to read the compiler's response instead of the UI.&lt;/p&gt;

&lt;p&gt;One rule from that period is now a team policy: &lt;strong&gt;automate the UI only when there is no CLI or API.&lt;/strong&gt; Browser automation is fragile, slow and breaks with every redesign. It is the last resort, not the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "Check" really runs
&lt;/h3&gt;

&lt;p&gt;Here is the chain on the server. The button sends the widget to the widget service. That service puts a "compile" job on a RabbitMQ queue. A Node.js worker picks it up. Inside the worker is a plain TypeScript compiler.&lt;/p&gt;

&lt;p&gt;The interesting part happens before compilation. For each widget, the platform generates a type definition file describing the world the script lives in: the form's context and fields, the fields of the app the form is bound to, the server functions, the platform SDK. That is how the compiler knows your contract has a &lt;code&gt;counterparty&lt;/code&gt; field and does not have &lt;code&gt;counterpartyy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then it is a normal TypeScript program of two files: the generated definitions and your script. Any diagnostic rejects the script and nothing is stored. Otherwise the code is emitted. Client code gets wrapped in a function roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ViewContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;System&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// your code, imports stripped&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSave&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSave&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every top-level function is returned so the platform can call it. Server code is transpiled in place. The platform also stores a description of each function's parameters, and those descriptions contain the literal string &lt;code&gt;"[object Object]"&lt;/code&gt; where types should be, an artifact of how they are built from the syntax tree. Harmless, but if you want byte-identical output, you have to reproduce quirks like that too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the compiler locally
&lt;/h3&gt;

&lt;p&gt;If the compiler is ordinary, you can run it yourself. We did.&lt;/p&gt;

&lt;p&gt;The per-widget type definitions come from the same request the designer itself makes, with a normal login. No tokens are pulled out of the browser. The compiler is the public &lt;code&gt;typescript&lt;/code&gt; package pinned to the exact server version, with the same options. A test fails if the version drifts. The wrapper and emit steps were ported line by line.&lt;/p&gt;

&lt;p&gt;Then we had to prove it was the same compiler, not a similar one. We took the whole contracts module, all 74 widgets, compiled each one locally, sent the same sources to the server's own check, and compared byte by byte.&lt;/p&gt;

&lt;p&gt;The first run showed type errors in ten widgets. They were not bugs in the code but gaps in our understanding of how the server builds the type definitions. For example, for a form bound to an app, fields come from the app, and the form's own fields arrive as an extra context. Each rule cost one failing widget. Once the rules were complete, all 74 matched. Deliberately injected errors (a string assigned to a number, a call to a missing server function, a wrong return type) were reported by both sides at the same position.&lt;/p&gt;

&lt;p&gt;Seven stock widgets stored compiled code in an older wrapper format. When we sent them to the server compiler, it produced our output, so that code was simply built by an older platform version.&lt;/p&gt;

&lt;p&gt;We also found a blind spot in the server's own Check: it builds the list of server functions from what is already saved, not from the submitted code. A brand-new server function looks like an error during the first check, even though it publishes and runs fine. Our tool recognizes that exact pattern and says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  "There is no signature"
&lt;/h3&gt;

&lt;p&gt;Why couldn't a package with a tiny manual edit go back in? Our first guess was a signature.&lt;/p&gt;

&lt;p&gt;It wasn't. We changed one letter in a panel title. The edited package passed server validation. The only hashes in the package protect the "paid" and "locked" flags, so a paid solution cannot be flipped to free. Form and script content is not covered. We even flipped a character in the company identifier inside the package. Nothing changed.&lt;/p&gt;

&lt;p&gt;The real gate is version identity. Every solution has an authoring server, where it was created, and target servers, where it is installed. On a target, import gives the widget a fresh internal ID and new versions land cleanly. On the authoring server, import matches the existing widget and tries to write a new version whose ID is derived from the version number. That record already exists, so the server reports a collision.&lt;/p&gt;

&lt;p&gt;So: the server that authored a solution refuses a new version of its own widget from a file. The same package installs fine where the solution is merely installed. That is not tamper protection. It is an identity rule, and it should shape how you arrange your environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The silent success trap
&lt;/h3&gt;

&lt;p&gt;The next traps don't fail. They pretend everything is fine.&lt;/p&gt;

&lt;p&gt;Each object in the package carries a version history. If you change an object without adding a new history entry, the import sees a match with the server and silently skips it. Exit code zero, task successful, nothing applied. Pre-validation doesn't help: it is equally happy with a package that will apply, one that will be skipped, and one that will collide.&lt;/p&gt;

&lt;p&gt;Worse, the server does not recompile on import. It stores the compiled code from the package as-is. Fix the source but skip the compile, and the designer shows your fix while the form runs the old code.&lt;/p&gt;

&lt;p&gt;Two rules came out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After every install, export again and diff&lt;/strong&gt; against what you sent. Exit codes are not proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the local compiler in verify mode in CI.&lt;/strong&gt; It writes nothing and fails if any widget's compiled code does not match its source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The vendor update that would have deleted our work
&lt;/h3&gt;

&lt;p&gt;A supplier sent a new build of the contracts module to install over ours. Before installing, we exported the live solution and diffed the forms. The sidebar of the contract card had eleven blocks; so did the update, but different ones. The block containing our custom document-list widget was simply not there. Installed as is, the customization would have quietly vanished from the form.&lt;/p&gt;

&lt;p&gt;No bad intent: the supplier didn't know about our change. But the rule is now fixed. Before installing any update package, diff the live forms. An update is a full new version of a form, not a patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipeline today
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export solution from the environment
        |
edit .ts source in the repo (human or agent)
        |
compile locally against that environment's types
   |- type error -&amp;gt; nothing written, fix it
   |- ok -&amp;gt; compiled code regenerated, version entry added
        |
review the diff: source + compiled code
        |
install on the test server
        |
export again, compare with what was sent
        |
commit what is actually live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before installing, the tooling snapshots the target. Rollback goes through files too, with a twist: a raw old snapshot is refused because its version history is older than the server's. So rollback takes the content from the snapshot, the history from the server, adds a new entry, and verifies by exporting again.&lt;/p&gt;

&lt;p&gt;Environments have roles. A sandbox we fully control for experiments. A shared development server for business work. Then test and production, where a partner company transfers changes through its own process. Production is read-only for our agents. Before anything goes to the shared server, a candidate is installed on an isolated environment and the whole affected business route is exercised with real form buttons. A successful publish response only proves the request arrived.&lt;/p&gt;

&lt;p&gt;Finished changes go into a simple register. One card per change: what changes, how to check it on test, how to confirm it on production read-only. Statuses: waiting for transfer, on Test, on PROD, or cancelled. Boring, and it stops finished work from sitting forgotten for two weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI for our own tooling
&lt;/h3&gt;

&lt;p&gt;Around this pipeline we built an internal team web service, our Wiki. Agents work on it too, so its CI is stricter. Every push and pull request runs the tests, then a set of real-browser checks walks through the pages, and their screenshots are kept for a week.&lt;/p&gt;

&lt;p&gt;The production update command only runs from a clean main branch and ships an archive of exactly the committed revision. It builds and tests on the host while the old service keeps running, and stops the old one only after the new one passes. It takes a backup, keeps the previous container for rollback, checks health and HTTPS afterwards, and restarts the old container if activation fails. It connects only to a host whose key is already known and refuses unknown or changed keys. No secrets go into the repo, the command line or the release archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;Low-code is development. Once you have more than one developer, or a single AI agent, you need files, diffs, review and repeatable installs.&lt;/p&gt;

&lt;p&gt;"No documented path" doesn't always mean "no path". Sometimes it means looking at what the button does. Behind ours was an ordinary TypeScript compiler, and we could reproduce it honestly, without bypassing any protection.&lt;/p&gt;

&lt;p&gt;And the most dangerous failures are the quiet ones. We no longer trust buttons or exit codes. We trust the re-export and the diff.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>typescript</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>We reverse-engineered a low-code platform's "Check" button and put its widget code in git</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:41:34 +0000</pubDate>
      <link>https://dev.to/netbka/we-reverse-engineered-a-low-code-platforms-check-button-and-put-its-widget-code-in-git-74l</link>
      <guid>https://dev.to/netbka/we-reverse-engineered-a-low-code-platforms-check-button-and-put-its-widget-code-in-git-74l</guid>
      <description>&lt;p&gt;Low-code platforms promise you will barely write code. That holds for a few months. Then you have dozens of forms, several people editing them, and someone who "just tweaked a script in production". It turns out you are doing software development after all, only without git, without review and without a way to roll back.&lt;/p&gt;

&lt;p&gt;I lead a business applications team in a holding company. We build modules for contracts, counterparties, correspondence and powers of attorney on ELMA 365, a low-code BPM platform popular in Russian-speaking markets. Part of the code is written by AI coding agents. This post is about the pipeline we built to get widget code out of the browser and into a repository, and the traps we found on the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  The starting point
&lt;/h3&gt;

&lt;p&gt;In ELMA 365, almost everything is configured in a web designer: forms, fields, processes, permissions. Widgets carry TypeScript scripts. Client scripts run in the user's browser, server scripts run on the platform. You write them in a code editor inside the designer, press "Check", press "Publish", done.&lt;/p&gt;

&lt;p&gt;That works for one developer. For a team where AI agents write code, it is a dead end. An agent needs a file. A reviewer needs a diff. A manager needs to know what changed on Tuesday.&lt;/p&gt;

&lt;p&gt;The platform ships a CLI that exports a solution to files. Export works well: hundreds of files, forms, processes, permissions and scripts, all ready for git. Import is the problem. Label and form changes went back fine. As soon as a widget script changed, the import failed. We reproduced it on several platform builds. The official position is clear: widget scripts are written in the designer, and there is no documented path around it.&lt;/p&gt;

&lt;p&gt;We could have accepted that. Instead we went to see what the button actually does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stopgap: a robot in the designer
&lt;/h3&gt;

&lt;p&gt;While we investigated, work had to ship. So the first solution was an honest hack: a headless browser that logs in, opens the widget, pastes the code, and clicks Save, Check and Publish.&lt;/p&gt;

&lt;p&gt;Every step cost an evening. The code editor auto-closes brackets, so typing code character by character produces garbage. You paste through the clipboard instead. You cannot verify the paste by reading the hidden textarea either, because it only mirrors the visible part of the file.&lt;/p&gt;

&lt;p&gt;The worst surprise came after a platform upgrade. In the new UI, the Publish button stayed enabled even when Check had reported compile errors. A robot that looked at button state could have published broken code. We changed it to read the compiler's response instead of the UI.&lt;/p&gt;

&lt;p&gt;One rule from that period is now a team policy: &lt;strong&gt;automate the UI only when there is no CLI or API.&lt;/strong&gt; Browser automation is fragile, slow and breaks with every redesign. It is the last resort, not the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "Check" really runs
&lt;/h3&gt;

&lt;p&gt;Here is the chain on the server. The button sends the widget to the widget service. That service puts a "compile" job on a RabbitMQ queue. A Node.js worker picks it up. Inside the worker is a plain TypeScript compiler.&lt;/p&gt;

&lt;p&gt;The interesting part happens before compilation. For each widget, the platform generates a type definition file describing the world the script lives in: the form's context and fields, the fields of the app the form is bound to, the server functions, the platform SDK. That is how the compiler knows your contract has a &lt;code&gt;counterparty&lt;/code&gt; field and does not have &lt;code&gt;counterpartyy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then it is a normal TypeScript program of two files: the generated definitions and your script. Any diagnostic rejects the script and nothing is stored. Otherwise the code is emitted. Client code gets wrapped in a function roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ViewContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;System&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// your code, imports stripped&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onInit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSave&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;onInit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSave&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every top-level function is returned so the platform can call it. Server code is transpiled in place. The platform also stores a description of each function's parameters, and those descriptions contain the literal string &lt;code&gt;"[object Object]"&lt;/code&gt; where types should be, an artifact of how they are built from the syntax tree. Harmless, but if you want byte-identical output, you have to reproduce quirks like that too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running the compiler locally
&lt;/h3&gt;

&lt;p&gt;If the compiler is ordinary, you can run it yourself. We did.&lt;/p&gt;

&lt;p&gt;The per-widget type definitions come from the same request the designer itself makes, with a normal login. No tokens are pulled out of the browser. The compiler is the public &lt;code&gt;typescript&lt;/code&gt; package pinned to the exact server version, with the same options. A test fails if the version drifts. The wrapper and emit steps were ported line by line.&lt;/p&gt;

&lt;p&gt;Then we had to prove it was the same compiler, not a similar one. We took the whole contracts module, all 74 widgets, compiled each one locally, sent the same sources to the server's own check, and compared byte by byte.&lt;/p&gt;

&lt;p&gt;The first run showed type errors in ten widgets. They were not bugs in the code but gaps in our understanding of how the server builds the type definitions. For example, for a form bound to an app, fields come from the app, and the form's own fields arrive as an extra context. Each rule cost one failing widget. Once the rules were complete, all 74 matched. Deliberately injected errors (a string assigned to a number, a call to a missing server function, a wrong return type) were reported by both sides at the same position.&lt;/p&gt;

&lt;p&gt;Seven stock widgets stored compiled code in an older wrapper format. When we sent them to the server compiler, it produced our output, so that code was simply built by an older platform version.&lt;/p&gt;

&lt;p&gt;We also found a blind spot in the server's own Check: it builds the list of server functions from what is already saved, not from the submitted code. A brand-new server function looks like an error during the first check, even though it publishes and runs fine. Our tool recognizes that exact pattern and says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  "There is no signature"
&lt;/h3&gt;

&lt;p&gt;Why couldn't a package with a tiny manual edit go back in? Our first guess was a signature.&lt;/p&gt;

&lt;p&gt;It wasn't. We changed one letter in a panel title. The edited package passed server validation. The only hashes in the package protect the "paid" and "locked" flags, so a paid solution cannot be flipped to free. Form and script content is not covered. We even flipped a character in the company identifier inside the package. Nothing changed.&lt;/p&gt;

&lt;p&gt;The real gate is version identity. Every solution has an authoring server, where it was created, and target servers, where it is installed. On a target, import gives the widget a fresh internal ID and new versions land cleanly. On the authoring server, import matches the existing widget and tries to write a new version whose ID is derived from the version number. That record already exists, so the server reports a collision.&lt;/p&gt;

&lt;p&gt;So: the server that authored a solution refuses a new version of its own widget from a file. The same package installs fine where the solution is merely installed. That is not tamper protection. It is an identity rule, and it should shape how you arrange your environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The silent success trap
&lt;/h3&gt;

&lt;p&gt;The next traps don't fail. They pretend everything is fine.&lt;/p&gt;

&lt;p&gt;Each object in the package carries a version history. If you change an object without adding a new history entry, the import sees a match with the server and silently skips it. Exit code zero, task successful, nothing applied. Pre-validation doesn't help: it is equally happy with a package that will apply, one that will be skipped, and one that will collide.&lt;/p&gt;

&lt;p&gt;Worse, the server does not recompile on import. It stores the compiled code from the package as-is. Fix the source but skip the compile, and the designer shows your fix while the form runs the old code.&lt;/p&gt;

&lt;p&gt;Two rules came out of this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;After every install, export again and diff&lt;/strong&gt; against what you sent. Exit codes are not proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run the local compiler in verify mode in CI.&lt;/strong&gt; It writes nothing and fails if any widget's compiled code does not match its source.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The vendor update that would have deleted our work
&lt;/h3&gt;

&lt;p&gt;A supplier sent a new build of the contracts module to install over ours. Before installing, we exported the live solution and diffed the forms. The sidebar of the contract card had eleven blocks; so did the update, but different ones. The block containing our custom document-list widget was simply not there. Installed as is, the customization would have quietly vanished from the form.&lt;/p&gt;

&lt;p&gt;No bad intent: the supplier didn't know about our change. But the rule is now fixed. Before installing any update package, diff the live forms. An update is a full new version of a form, not a patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pipeline today
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export solution from the environment
        |
edit .ts source in the repo (human or agent)
        |
compile locally against that environment's types
   |- type error -&amp;gt; nothing written, fix it
   |- ok -&amp;gt; compiled code regenerated, version entry added
        |
review the diff: source + compiled code
        |
install on the test server
        |
export again, compare with what was sent
        |
commit what is actually live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before installing, the tooling snapshots the target. Rollback goes through files too, with a twist: a raw old snapshot is refused because its version history is older than the server's. So rollback takes the content from the snapshot, the history from the server, adds a new entry, and verifies by exporting again.&lt;/p&gt;

&lt;p&gt;Environments have roles. A sandbox we fully control for experiments. A shared development server for business work. Then test and production, where a partner company transfers changes through its own process. Production is read-only for our agents. Before anything goes to the shared server, a candidate is installed on an isolated environment and the whole affected business route is exercised with real form buttons. A successful publish response only proves the request arrived.&lt;/p&gt;

&lt;p&gt;Finished changes go into a simple register. One card per change: what changes, how to check it on test, how to confirm it on production read-only. Statuses: waiting for transfer, on Test, on PROD, or cancelled. Boring, and it stops finished work from sitting forgotten for two weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI for our own tooling
&lt;/h3&gt;

&lt;p&gt;Around this pipeline we built an internal team web service, our Wiki. Agents work on it too, so its CI is stricter. Every push and pull request runs the tests, then a set of real-browser checks walks through the pages, and their screenshots are kept for a week.&lt;/p&gt;

&lt;p&gt;The production update command only runs from a clean main branch and ships an archive of exactly the committed revision. It builds and tests on the host while the old service keeps running, and stops the old one only after the new one passes. It takes a backup, keeps the previous container for rollback, checks health and HTTPS afterwards, and restarts the old container if activation fails. It connects only to a host whose key is already known and refuses unknown or changed keys. No secrets go into the repo, the command line or the release archive.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took away
&lt;/h3&gt;

&lt;p&gt;Low-code is development. Once you have more than one developer, or a single AI agent, you need files, diffs, review and repeatable installs.&lt;/p&gt;

&lt;p&gt;"No documented path" doesn't always mean "no path". Sometimes it means looking at what the button does. Behind ours was an ordinary TypeScript compiler, and we could reproduce it honestly, without bypassing any protection.&lt;/p&gt;

&lt;p&gt;And the most dangerous failures are the quiet ones. We no longer trust buttons or exit codes. We trust the re-export and the diff.&lt;/p&gt;

</description>
      <category>lowcode</category>
      <category>typescript</category>
      <category>cicd</category>
    </item>
    <item>
      <title>A read-only first week: three questions to ask your computers</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Sat, 24 Oct 2026 11:47:29 +0000</pubDate>
      <link>https://dev.to/netbka/a-read-only-first-week-three-questions-to-ask-your-computers-25do</link>
      <guid>https://dev.to/netbka/a-read-only-first-week-three-questions-to-ask-your-computers-25do</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://localcmd.app/use-cases/read-only-first-week" rel="noopener noreferrer"&gt;localcmd.app/use-cases/read-only-first-week&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most people's first worry about letting an AI near their computers is the same: what if it deletes something. So don't start there. Spend the first week on questions that only need looking.&lt;/p&gt;

&lt;p&gt;Here are three. Each one comes with the manual way first, because you should be able to do it without any tool, and then what it looks like when your AI does it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why is my disk full?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;By hand.&lt;/strong&gt; On Windows, open PowerShell and list the biggest top-level folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;C:\&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Directory&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FullName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Measure-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Sum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="nx"&gt;Folder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;Round&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GB&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Mac or Linux machine, &lt;code&gt;du -xsk ~/* | sort -rn | head&lt;/code&gt; does the same for your home folder.&lt;/p&gt;

&lt;p&gt;Two things catch people out. First, a full scan of a big disk takes minutes, not seconds; on my Windows machine it took about two and a half. Second, on a Mac the obvious command lies: &lt;code&gt;df -h /&lt;/code&gt; shows only the small system volume. Your files live on a separate one, so check &lt;code&gt;df -h /System/Volumes/Data&lt;/code&gt; instead. The folder sizes also never add up to the used space, because of system files and folders you don't have rights to read. Expect a gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With your AI.&lt;/strong&gt; Ask: "Why is the disk on my work PC almost full? Show me the ten biggest folders and tell me what each one is. Don't delete anything." You get a ranked list in plain words ("this is an app cache, that is your photo library, that is old installers") instead of a wall of numbers. The deleting, if any, is a separate step that starts only after you say yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why is it slow, or why did it restart?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;By hand.&lt;/strong&gt; How long has it been on, what is using the memory, how many programs start with it, and did it restart by itself. On Windows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_OperatingSystem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"Uptime hours: "&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastBootUpTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TotalHours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Descending&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-First&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'MB'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WS&lt;/span&gt;&lt;span class="n"&gt;/1MB&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;CimInstance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Win32_StartupCommand&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Measure&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Object&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;WinEvent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;FilterHashtable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;LogName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'System'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1074&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6008&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;StartTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Date&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;AddDays&lt;/span&gt;&lt;span class="err"&gt;(-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Mac, &lt;code&gt;uptime&lt;/code&gt;, &lt;code&gt;last reboot | head&lt;/code&gt; and &lt;code&gt;ps -Aceo rss,comm -m | head&lt;/code&gt; cover the same ground.&lt;/p&gt;

&lt;p&gt;A quiet answer is still an answer. If the restart log is empty, nothing restarted it, and your slow day has another cause. One of my machines had been running for 53 days without a reboot, which explained more than any single program did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With your AI.&lt;/strong&gt; Ask: "My laptop feels slow. Check how long it has been on, what uses the memory, what starts at login and whether it restarted recently, then tell me the most likely cause." The useful part is the last sentence. You get one conclusion, or an honest "I didn't find a clear cause", and you can ask for the evidence behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Did my backup copy everything?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;By hand.&lt;/strong&gt; Compare the original and the copy. On a Mac or Linux machine &lt;code&gt;diff -rq source backup&lt;/code&gt; lists files that are missing or differ. On Windows, hash both folders and compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Resolve-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;\backup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Substring&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$s&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-ChildItem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Recurse&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-File&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pscustomobject&lt;/span&gt;&lt;span class="p"&gt;]@{&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Substring&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Length&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;H&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Get&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;FileHash&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FullName&lt;/span&gt;&lt;span class="err"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Compare-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Property&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;H&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried this on a test folder where I had deleted one file and edited another in the copy. Both showed up straight away: one missing, one changed. A comparison by size and date is faster; one by content is slower but catches more. Say which one you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With your AI.&lt;/strong&gt; Ask: "Compare the Photos folder on my desktop with the copy on the external drive. List what is missing or different. Don't copy or fix anything." You get a short list of exceptions instead of a feeling that it probably worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why start here
&lt;/h2&gt;

&lt;p&gt;None of these changes a file. If the AI misreads something, the worst result is a wrong sentence, and you can check it against the commands above. After a week of that, you know how it answers, how it explains itself and how often it says "I'm not sure". That is a better basis for the day you let it change something.&lt;/p&gt;

&lt;p&gt;I build Local Commander, so this is the setup I tried it with: a small agent on each computer, and your ChatGPT or Claude chat connects to it. Every example above ran on my own Windows PC and my own Mac. It works through commands and files, not through the screen, so it won't click a dialog for you, and the computer has to be on. You choose what each computer allows, and you can switch any of them off.&lt;/p&gt;

&lt;p&gt;If you try the three questions on your own machines, I'd like to hear which one gave a useful answer and which one didn't.&lt;/p&gt;

&lt;p&gt;Setup steps and the other first-week questions: &lt;a href="https://localcmd.app/use-cases/read-only-first-week" rel="noopener noreferrer"&gt;https://localcmd.app/use-cases/read-only-first-week&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>productivity</category>
      <category>powershell</category>
    </item>
    <item>
      <title>Exit code, stdout, stderr: the three answers a command gives you, and the one it doesn't</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:19:31 +0000</pubDate>
      <link>https://dev.to/netbka/exit-code-stdout-stderr-the-three-answers-a-command-gives-you-and-the-one-it-doesnt-30k1</link>
      <guid>https://dev.to/netbka/exit-code-stdout-stderr-the-three-answers-a-command-gives-you-and-the-one-it-doesnt-30k1</guid>
      <description>&lt;p&gt;Every program you run hands back three separate things. People read them as one. That is the whole bug this article is about.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;stdout&lt;/strong&gt;: normal output. Results, progress, "started".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stderr&lt;/strong&gt;: the error stream. Warnings and failures, if the author sent them there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;exit code&lt;/strong&gt;: one integer when the process ends. &lt;code&gt;0&lt;/code&gt; means "I did not report a failure". Anything else means "I did".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are independent channels. A program can print cheerful text to stdout and exit non-zero. It can print nothing and exit &lt;code&gt;0&lt;/code&gt;. It can write to stderr and still exit &lt;code&gt;0&lt;/code&gt; (many warnings do exactly that). None of the three tells you whether the thing you wanted now exists. Let's make that concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  A program that disagrees with itself on purpose
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;app.cjs&lt;/code&gt;. It writes a "started" line to stdout, then notices a required file is missing, complains on stderr and exits with &lt;code&gt;42&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;started synthetic-report&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;required-config.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONFIG_MISSING: required-config.json is absent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it in a folder without &lt;code&gt;required-config.json&lt;/code&gt;. I did this on Windows 11 with Node.js 22; the behaviour is the same on macOS and Linux.&lt;/p&gt;

&lt;h3&gt;
  
  
  In bash
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.cjs
&lt;span class="nb"&gt;echo&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;started synthetic-report
CONFIG_MISSING: required-config.json is absent
exit: 42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both lines land in the same terminal, so by eye they look like one message. They are not. Prove it by splitting the streams:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.cjs 2&amp;gt;errors.txt     &lt;span class="c"&gt;# stderr goes to a file, stdout stays on screen&lt;/span&gt;
&lt;span class="nb"&gt;echo&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="nb"&gt;cat &lt;/span&gt;errors.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  In PowerShell
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app.cjs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"exit: &lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same two lines, &lt;code&gt;exit: 42&lt;/code&gt;. Note the variable: for a native program, &lt;code&gt;$LASTEXITCODE&lt;/code&gt; holds the exit code. &lt;code&gt;$?&lt;/code&gt; only tells you whether the last &lt;em&gt;PowerShell&lt;/em&gt; statement succeeded, which is a different question and a common trap in scripts that mix the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interpretation mistake
&lt;/h2&gt;

&lt;p&gt;Here are three ways people turn that &lt;code&gt;42&lt;/code&gt; into "it worked".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reading stdout as the verdict.&lt;/strong&gt; The first line says &lt;code&gt;started&lt;/code&gt;. A script that greps for &lt;code&gt;started&lt;/code&gt;, or a person skimming, sees success. stdout is narration, not a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Losing the exit code in a pipe.&lt;/strong&gt; In bash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.cjs | &lt;span class="nb"&gt;tee &lt;/span&gt;run.log
&lt;span class="nb"&gt;echo&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="c"&gt;# 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;0&lt;/code&gt; is &lt;code&gt;tee&lt;/code&gt;'s exit code, not the app's. A pipeline's status is the status of its last command. Fix it with &lt;code&gt;set -o pipefail&lt;/code&gt; at the top of the script (then the pipeline fails if any stage fails), or read &lt;code&gt;${PIPESTATUS[0]}&lt;/code&gt; right after the pipe.&lt;/p&gt;

&lt;p&gt;PowerShell has the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app.cjs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Tee-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;run.log&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="s2"&gt;"exit: &lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c"&gt;# still 42 here, because node was the last *native* command&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PowerShell keeps &lt;code&gt;$LASTEXITCODE&lt;/code&gt; from the last native executable, so this particular pipeline is fine, but as soon as you wrap the call in a function or a &lt;code&gt;try&lt;/code&gt; block, check that the code is actually propagated and not swallowed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Trusting the wrapper's own exit.&lt;/strong&gt; Suppose a script runs the app and then writes a report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// run.cjs&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawnSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report written&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;&lt;code&gt;node run.cjs&lt;/code&gt; exits &lt;code&gt;0&lt;/code&gt;. The wrapper is telling the truth about itself: it did write a report. The child exited &lt;code&gt;42&lt;/code&gt;, and nobody looked. This is how CI steps, deploy scripts and AI assistants end up saying "done" over a failure. The wrapper has to carry the child's verdict forward:&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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app.cjs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;exitCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exitCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// null status means the child was killed by a signal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the report contains all three channels, and the wrapper's own exit code is honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that answers the real question
&lt;/h2&gt;

&lt;p&gt;Even a perfectly captured &lt;code&gt;42&lt;/code&gt; only tells you what the process thought. Your question was different: did the report get produced? Did the files get copied? Did the service come back up? The exit code is evidence about the process. The result needs its own check, against the artifact you wanted.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;app.cjs&lt;/code&gt; was meant to write &lt;code&gt;report.json&lt;/code&gt; (our synthetic one never gets that far). Then the real check is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node app.cjs&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"process failed: &lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$code&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; report.json &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"process exited 0 but report.json is missing or empty"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app.cjs&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-ne&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process failed: &lt;/span&gt;&lt;span class="nv"&gt;$LASTEXITCODE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;report.json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;report.json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"process exited 0 but report.json is missing or empty"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two checks, in that order. First the process's verdict (exit code, with stderr next to it so the failure has a name). Then the artifact: does the thing exist, is it non-empty, does it have the rows or files you expected. The second check is the one people skip, and it is the one that catches "exit 0, wrong result": a copy with the right file count and one changed file, a build that passed with the old bundle still on disk, a service that started and then died.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Print all three channels, as data, before you summarise. "It said something about config" is not evidence; &lt;code&gt;CONFIG_MISSING&lt;/code&gt;, exit &lt;code&gt;42&lt;/code&gt; is.&lt;/li&gt;
&lt;li&gt;Never let a pipe or a wrapper eat the exit code. &lt;code&gt;set -o pipefail&lt;/code&gt;, &lt;code&gt;${PIPESTATUS[0]}&lt;/code&gt;, &lt;code&gt;$LASTEXITCODE&lt;/code&gt;, &lt;code&gt;process.exitCode = child.status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Name the artifact before you run, and check for it after. A clean exit code is a reason to look, not a reason to stop.&lt;/li&gt;
&lt;li&gt;Treat a missing number as missing. Truncated output, no exit code captured, a timeout: that is "unknown", not "probably fine".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try the two-line &lt;code&gt;app.cjs&lt;/code&gt; in your own shell and in whatever runs your scripts for you. If you find a place where the &lt;code&gt;42&lt;/code&gt; disappears between the program and the summary, that is the bug worth fixing first.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Author note: I'm Peter. I build a tool that lets a chat run commands on your own computers, and this exact mistake is why I insist on seeing all three channels; the read-only checks I start with are at &lt;a href="https://localcmd.app/use-cases/read-only-first-week" rel="noopener noreferrer"&gt;https://localcmd.app/use-cases/read-only-first-week&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bash</category>
      <category>node</category>
      <category>beginners</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to connect ChatGPT or Claude on the web to your own computer, without opening a port</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 11:05:19 +0000</pubDate>
      <link>https://dev.to/netbka/how-to-connect-chatgpt-or-claude-on-the-web-to-your-own-computer-without-opening-a-port-pim</link>
      <guid>https://dev.to/netbka/how-to-connect-chatgpt-or-claude-on-the-web-to-your-own-computer-without-opening-a-port-pim</guid>
      <description>&lt;p&gt;You got a local MCP server working. Claude Desktop sees it, your editor sees it, and the model runs your build without you pasting anything. Then you open ChatGPT in a browser tab, or Claude on your phone on the way home, and the tools are gone. The chat is right there, the computer is right there, and the two never meet.&lt;/p&gt;

&lt;p&gt;Someone on r/mcp put it in one line: "ChatGPT and Claude web can't run local MCP servers." They can't, for a plain reason. Here's why, the two ways around it, and how to choose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the web chat can't see localhost
&lt;/h2&gt;

&lt;p&gt;When you add a connector in ChatGPT or Claude on the web, your browser doesn't talk to the MCP server. The provider's servers do: when the model wants a tool, their backend calls the URL you gave it. From there, &lt;code&gt;localhost&lt;/code&gt; means their machine, not yours, and your PC usually sits behind a home router without a public address anyway. The phone apps work the same way.&lt;/p&gt;

&lt;p&gt;So the web and phone apps can only use an MCP server at a public HTTPS address. Desktop apps can start a local server as a child process and talk to it over stdio, which is why the same server works in one place and not the other.&lt;/p&gt;

&lt;p&gt;That leaves two ways to give your computer a reachable endpoint without forwarding a port on your router.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option A: a tunnel to your own MCP server
&lt;/h2&gt;

&lt;p&gt;A tunnel tool such as cloudflared or ngrok runs on your computer, opens an outgoing connection to the tunnel provider, and gives you a public HTTPS URL that forwards to a local port. I started this way myself: a small server on my PC, a tunnel, and a login so not just anyone could walk in. It worked.&lt;/p&gt;

&lt;p&gt;The steps, at a high level:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serve MCP over HTTP.&lt;/strong&gt; Many local servers only speak stdio. You need one that speaks Streamable HTTP, or a small bridge that wraps a stdio server in HTTP. Bind it to &lt;code&gt;127.0.0.1&lt;/code&gt;, not to every network interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start the tunnel.&lt;/strong&gt; For example &lt;code&gt;cloudflared tunnel --url http://localhost:8080&lt;/code&gt; or &lt;code&gt;ngrok http 8080&lt;/code&gt;. Depending on the tool and setup, the URL may change on each start; a fixed hostname saves re-adding the connector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put OAuth in front.&lt;/strong&gt; Either the server implements MCP's OAuth-based authorization, or a proxy in front of it does. This is usually the bulk of the work, and it's the step you can't skip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the connector.&lt;/strong&gt; Paste the public URL, including the MCP path, as a custom connector in ChatGPT or Claude, and sign in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The risks come with the URL. The moment the tunnel is up, your tools have a public address. Without sign-in, anyone who finds the URL can call them, and a hard-to-guess URL isn't a password: URLs leak through logs, browser history and screenshots. Run the server as a normal user, never as administrator or root, so a bad call can't do more than that account can. Expose only the tools you need. Close the tunnel when you're done, not weeks later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option B: let the computer dial out
&lt;/h2&gt;

&lt;p&gt;The other pattern turns the connection around. A small agent on your computer opens an outgoing connection to a hosted MCP service and keeps it open. Your AI app connects to that service over HTTPS, with sign-in. When the model calls a tool, the service passes the request down the open connection to your computer, the agent runs it, and the result comes back the same way. Nothing on your computer listens on the internet, so there is no URL pointing at your machine to leak.&lt;/p&gt;

&lt;p&gt;The trade-off is trust: requests and their output pass through the hosted service, so you are trusting whoever runs it.&lt;/p&gt;

&lt;p&gt;I build Local Commander, which works this way, so I'm biased. The agent on each computer opens an outgoing WebSocket to localcmd.app and waits for work. Your AI app connects to &lt;code&gt;https://localcmd.app/mcp&lt;/code&gt; and signs in with OAuth. From then on, a message typed into Claude on your phone can run on the PC under your desk:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Home PC, run the tests in my project folder and tell me what failed and why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model reads files, runs commands, checks Git or asks a local web service whether it answers, on the computer you named. If more than one computer is online and you didn't say which, the server's instructions tell the model to ask instead of guessing. It works from ChatGPT, Claude, Cursor and other apps that support remote MCP with sign-in.&lt;/p&gt;

&lt;p&gt;Setup takes four steps and no code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install.&lt;/strong&gt; On each computer, run &lt;code&gt;npx local-commander-agent@latest&lt;/code&gt;. You need Node.js 20 or later, on Windows, macOS or Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve.&lt;/strong&gt; Confirm the pairing in your browser at localcmd.app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose access.&lt;/strong&gt; Your whole user account, which is the default once you approve, or only the folders you pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect your AI.&lt;/strong&gt; Add &lt;code&gt;https://localcmd.app/mcp&lt;/code&gt; as a remote MCP server in ChatGPT, Claude, Cursor or another app, and sign in with your Local Commander account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No router settings, no tunnel. If you have more than one computer, the same chat reaches all of them; I wrote about that in &lt;a href="https://netbka.hashnode.dev/how-to-run-one-task-on-all-your-computers-from-a-single-chatgpt-or-claude-chat" rel="noopener noreferrer"&gt;How to run one task on all your computers from a single ChatGPT or Claude chat&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to choose
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A tunnel&lt;/strong&gt;, if you already run your own MCP server, want to own every piece of it, and are ready to build and maintain the sign-in. The tunnel provider still carries the traffic, but the server, the tools and the login are yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A relay&lt;/strong&gt;, if you want several computers in one chat, don't want anything on your machines reachable from the internet, or don't want to run an OAuth setup yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neither&lt;/strong&gt;, if you only use a desktop app on one computer. A local MCP server over stdio is simpler.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;p&gt;For the relay route, and for Local Commander in particular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The computer has to be on.&lt;/strong&gt; Awake, online and running the agent. A sleeping laptop can't answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One agent per computer.&lt;/strong&gt; Each machine you want to reach needs its own install and approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commands and files, not the screen.&lt;/strong&gt; It isn't screen sharing, and there's no mouse control. If a job only works by clicking through an app, use remote desktop or a computer-use tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It isn't a sandbox.&lt;/strong&gt; Once a computer is approved, the AI can do what your user account can do there. Folder rules prevent accidents, like the wrong folder or a stray delete, but they aren't a wall. For stronger isolation, run the agent under a separate user account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output goes to your AI app.&lt;/strong&gt; Whatever a command returns goes back to the chat, and from there it follows that app's rules.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Do I need to open a port on my router for either option?&lt;/strong&gt;&lt;br&gt;
No. Both dial out from your computer. The difference is what ends up public: a tunnel gives your own server a public URL, while a relay gives your computer none. The public endpoint is the hosted service, behind sign-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this work from the phone app?&lt;/strong&gt;&lt;br&gt;
Yes, wherever the phone app supports the connector you added: it calls tools from the provider's side, like the web app. With Local Commander, the computer still has to be awake, online and running the agent.&lt;/p&gt;

&lt;p&gt;If you've hit this wall, I'd like to hear which route you took and where it got in your way. The setup steps are also on the guide page: &lt;a href="https://localcmd.app/control-multiple-computers-with-ai" rel="noopener noreferrer"&gt;https://localcmd.app/control-multiple-computers-with-ai&lt;/a&gt;. You can block commands and revoke a computer or an AI app at localcmd.app at any time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://localcmd.app/use-cases/connect-web-ai-to-your-computer" rel="noopener noreferrer"&gt;localcmd.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>chatgpt</category>
      <category>devops</category>
    </item>
    <item>
      <title>How to run one task on all your computers from a single ChatGPT or Claude chat</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Fri, 23 Oct 2026 09:03:24 +0000</pubDate>
      <link>https://dev.to/netbka/how-to-run-one-task-on-all-your-computers-from-a-single-chatgpt-or-claude-chat-21g</link>
      <guid>https://dev.to/netbka/how-to-run-one-task-on-all-your-computers-from-a-single-chatgpt-or-claude-chat-21g</guid>
      <description>&lt;p&gt;Few people who work with computers have just one. There's the work machine, the PC at home, maybe a small server or a cloud VM, and a parent's laptop in another city that only gets attention when it breaks.&lt;/p&gt;

&lt;p&gt;Each of them is a separate trip. Open a remote session, SSH in, walk to the other room, or talk your mom through Settings over the phone. Then do the same thing again on the next machine.&lt;/p&gt;

&lt;p&gt;The AI chat you already use knows most of the commands for those jobs. What it lacks is a way onto your computers. This post shows one way to give it that: real prompts, the setup, and where it stops being the right tool.&lt;/p&gt;

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

&lt;p&gt;I build Local Commander, so this is the setup I know best. It has two parts. A small agent runs on each computer under your user account; it opens an outgoing connection to localcmd.app and waits for work, so there's no port forwarding to set up. Your AI app connects to the hosted MCP server at &lt;code&gt;https://localcmd.app/mcp&lt;/code&gt; (Streamable HTTP, OAuth sign-in). When you ask for something, the model calls tools on that server, such as read a file, search, run a command, check Git or ask a local web service whether it answers, and each call runs on the computer it was addressed to. Independent tasks on different computers run side by side; work on the same folder waits its turn. If more than one computer is online and you didn't say which one, the server's instructions tell the model to ask instead of guessing. It works from ChatGPT, Claude, Cursor and other apps that support remote MCP with sign-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to ask
&lt;/h2&gt;

&lt;p&gt;Name computers the way they appear in your account. The prompts below are examples; change the names and details to match yours.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Which server needs attention?
&lt;/h3&gt;

&lt;p&gt;You have a few machines that are supposed to just run. Instead of logging in to each one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check disk space, memory and failed services on all my servers and tell me which one needs attention.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model sends the same kind of check to each computer, reads what comes back and gives you one answer instead of a row of terminal windows. Then follow up with "Show me the command and output you based that on." Ask for the evidence, not only the verdict.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Same project, two machines, different results
&lt;/h3&gt;

&lt;p&gt;The build passes on your desktop and fails on the laptop. Before anyone touches code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Compare this project on Work PC and Laptop: Git revision, Node and package-manager versions, the lockfile and the build command. Then run the build on both at the same time and show me the first difference that matters, as a table with one column per computer. Don't install anything or edit files.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the two machines are on different revisions, you see that before anyone starts blaming the environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Every computer in the family
&lt;/h3&gt;

&lt;p&gt;Disk space is the classic. Include the machines you would otherwise have to visit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Check free disk space on every computer, including Mom's laptop. On any with less than 10% free, list the biggest folders in the user's profile. Don't delete or move anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When one of them turns out to be the real problem, stay in the same chat and narrow it down:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Mom's laptop, find out why her photo app no longer starts. Check its error logs, whether it is running, and free disk space. Explain it in plain words I can read to her over the phone. Don't uninstall anything, restart the computer or change settings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The "don't" lines are worth keeping in any prompt that only needs to look. They steer the model, but they are not a lock; that is what access settings are for (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Set it up in four steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install.&lt;/strong&gt; On each computer, run &lt;code&gt;npx local-commander-agent@latest&lt;/code&gt;. You need Node.js 20 or later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve.&lt;/strong&gt; Confirm the pairing in your browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose access.&lt;/strong&gt; Your whole user account, which is the default once you approve, or only the folders you pick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect your AI.&lt;/strong&gt; Add &lt;code&gt;https://localcmd.app/mcp&lt;/code&gt; as a remote MCP server in ChatGPT, Claude, Cursor or another app, and sign in with your Local Commander account.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's no code to write. Do the install before you need it: a laptop in another city has to be set up while someone is sitting at it, ideally its owner, and with their permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits, stated plainly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not a sandbox.&lt;/strong&gt; Once a computer is approved, the AI can do what your user account can do there, like you at the keyboard. Folder rules prevent accidents such as the wrong folder or a stray delete, but they aren't a wall. For stronger isolation, run the agent under a separate user account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The computer has to be reachable.&lt;/strong&gt; Awake, online and running the agent. A machine that is off or offline can't be reached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dropped connections aren't papered over.&lt;/strong&gt; If a computer drops in the middle of a task, the work is not re-run on a guess. Each request ends as done, failed or unknown, and an unknown one waits for you to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works on your computers, not your network.&lt;/strong&gt; The server's instructions tell the model to decline port scans and probes of other hosts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model can be wrong.&lt;/strong&gt; It can misread output or pick the wrong fix. Ask for the commands and output behind an answer before you act on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output goes to your AI app.&lt;/strong&gt; Whatever a command returns goes back to the AI app, and from there it follows that app's rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can block commands and revoke a computer or an AI app at localcmd.app at any time.&lt;/p&gt;

&lt;h2&gt;
  
  
  When something else fits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tasks that only work through the screen&lt;/strong&gt;, such as clicking through an app with no command line. Use remote desktop or a computer-use tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable admin jobs across many servers.&lt;/strong&gt; Have the AI write the script, then run it with Ansible or over SSH against an inventory you keep. That gives you something you can review and rerun, which a chat doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A managed company fleet with patching and compliance.&lt;/strong&gt; That is what remote monitoring and management (RMM) products are for; this doesn't replace them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One computer and one AI app.&lt;/strong&gt; That app's own computer features may be simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web tasks that don't touch files on your machines.&lt;/strong&gt; A cloud agent needs nothing installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One computer, nothing hosted.&lt;/strong&gt; The open-source edition (MIT) is a local MCP server for desktop clients such as Claude Desktop, Cursor and Cline, and runs without an account: &lt;a href="https://github.com/netbka/local-commander-mcp-agent" rel="noopener noreferrer"&gt;https://github.com/netbka/local-commander-mcp-agent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Can I do this from my phone?&lt;/strong&gt;&lt;br&gt;
Yes, through any AI app on your phone that is connected to Local Commander. The computer must be awake, online and running the agent. If your deploy runs from your work PC, you can start it from a train, as long as that PC is on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to write code?&lt;/strong&gt;&lt;br&gt;
No. Install the agent, approve it, and type requests in the AI chat you already use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it run the same task on all my computers at once?&lt;/strong&gt;&lt;br&gt;
Yes. Ask for it in one message. Independent tasks run side by side; work on the same folder waits its turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which systems does the agent support?&lt;/strong&gt;&lt;br&gt;
Windows 11 x64, macOS 14 or later on Apple Silicon, and Linux with a systemd user session (Ubuntu 24.04 x64 or arm64), with Node.js 20 or later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it show me the other computer's screen?&lt;/strong&gt;&lt;br&gt;
It isn't a screen-sharing tool. It works through commands, files and logs, with no mouse control. If a fix needs someone to click through a dialog, use screen sharing.&lt;/p&gt;

&lt;p&gt;If your machines are spread across more than one place, I'd like to hear which job you would hand over first, and where this setup gets in your way. The same steps are on the guide page: &lt;a href="https://localcmd.app/control-multiple-computers-with-ai" rel="noopener noreferrer"&gt;https://localcmd.app/control-multiple-computers-with-ai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://localcmd.app/use-cases/run-one-task-on-all-your-computers" rel="noopener noreferrer"&gt;localcmd.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why your coding agent keeps breaking on Windows, and four fixes that hold</title>
      <dc:creator>Peter Mamrukov</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:44:51 +0000</pubDate>
      <link>https://dev.to/netbka/why-your-coding-agent-keeps-breaking-on-windows-and-four-fixes-that-hold-2poh</link>
      <guid>https://dev.to/netbka/why-your-coding-agent-keeps-breaking-on-windows-and-four-fixes-that-hold-2poh</guid>
      <description>&lt;p&gt;For a while my workflow with AI was: the chat suggests a fix, I paste it into the terminal, run the tests, copy the error back. I was a very slow USB cable between the model and my Windows PC. Agent modes were supposed to end that. On Windows they often don't, and the reasons are fixable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two failures you've probably seen
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The agent writes a command your shell won't parse.&lt;/strong&gt; It chains steps with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; in Windows PowerShell 5.1, mangles backslashes, or runs a bash-style command that resolves to a different Windows program. A Cursor forum thread calls &lt;a href="https://forum.cursor.com/t/powershell-in-cursor-is-consistently-broken-endless-syntax-errors-not-isolated-cases/149548" rel="noopener noreferrer"&gt;PowerShell in Cursor "consistently broken"&lt;/a&gt;, and a Claude Code issue reports that &lt;a href="https://github.com/anthropics/claude-code/issues/5049" rel="noopener noreferrer"&gt;native Windows runs aren't really shell aware&lt;/a&gt;: &lt;code&gt;move&lt;/code&gt; not found, backslashes broken, &lt;code&gt;find&lt;/code&gt; resolving to the Windows binary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent starts a dev server and the chat never comes back.&lt;/strong&gt; &lt;code&gt;npm run dev&lt;/code&gt; doesn't exit, so the tool call waits forever, or the agent decides the server started when it didn't. Kill the terminal and the server often keeps its port; &lt;a href="https://forum.cursor.com/t/cursor-occupying-ports-and-not-freeing-up-when-the-terminal-window-is-killed/134793" rel="noopener noreferrer"&gt;one Cursor user&lt;/a&gt; put it as "I have to manually kill it."&lt;/p&gt;

&lt;p&gt;Both come from the same place. The agent is handed a terminal and expected to type into it like a person. A person knows which shell they're in and when to stop waiting. A model guesses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: pass a program and its arguments, not a command string
&lt;/h2&gt;

&lt;p&gt;Most quoting bugs disappear when nothing parses a command line. If you write your own agent tools or scripts, start real executables directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Node: no shell in between&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;execFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--short&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;work&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;my-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python: a list, and no shell=True
&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--short&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cwd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;C:\work\my-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One Windows catch: &lt;code&gt;npm&lt;/code&gt;, &lt;code&gt;npx&lt;/code&gt;, &lt;code&gt;pnpm&lt;/code&gt; and &lt;code&gt;yarn&lt;/code&gt; are usually &lt;code&gt;.cmd&lt;/code&gt; shims, and since Node's April 2024 security release, spawning a &lt;code&gt;.cmd&lt;/code&gt; or &lt;code&gt;.bat&lt;/code&gt; file needs &lt;code&gt;shell: true&lt;/code&gt;, so &lt;code&gt;cmd.exe&lt;/code&gt; gets involved after all. Keep that safe by refusing arguments that contain cmd metacharacters (&lt;code&gt;&amp;amp; | &amp;lt; &amp;gt; ^ % !&lt;/code&gt;) and quoting each argument yourself, or by running &lt;code&gt;node&lt;/code&gt; on the tool's JavaScript entry point. If your agent talks to tools over MCP, prefer a command tool that takes &lt;code&gt;command&lt;/code&gt; and &lt;code&gt;args&lt;/code&gt; as separate fields over one that takes a shell string.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: stop PowerShell-only mistakes before they run
&lt;/h2&gt;

&lt;p&gt;A rule in your agent's instructions is a hint; a hook is enforced. In Claude Code, a &lt;code&gt;PreToolUse&lt;/code&gt; hook with matcher &lt;code&gt;PowerShell&lt;/code&gt; sees each command first, and exit code 2 blocks it and sends your message back to the model:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PowerShell has no heredoc. Write the file with the Write tool, or use a @&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;@ here-string.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model reads the message and retries another way. The same pattern catches &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; on Windows PowerShell 5.1, or helper functions named after built-in aliases such as &lt;code&gt;rd&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: treat dev servers as processes, not terminal sessions
&lt;/h2&gt;

&lt;p&gt;Start long-running servers in the background and come back for the log, instead of waiting for them to exit. When one is left behind, the port leads you to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight batchfile"&gt;&lt;code&gt;&lt;span class="nb"&gt;netstat&lt;/span&gt; &lt;span class="na"&gt;-ano &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;findstr&lt;/span&gt; :5173
&lt;span class="nb"&gt;taskkill&lt;/span&gt; &lt;span class="na"&gt;/PID &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kd"&gt;pid&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="na"&gt;/T /F
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The PID is the last column of the LISTENING line. &lt;code&gt;/T&lt;/code&gt; stops the process together with everything it started (for Vite that includes esbuild); plain &lt;code&gt;Stop-Process&lt;/code&gt; only takes the one PID. And before believing "the server is running", ask it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl.exe &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; NUL &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; http://localhost:5173/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;curl.exe&lt;/code&gt; ships with current Windows 10 and 11; a &lt;code&gt;200&lt;/code&gt; is worth more than a log line that says "ready".&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: know where your guard rails end
&lt;/h2&gt;

&lt;p&gt;Limiting an agent to certain folders and keeping it away from &lt;code&gt;.env&lt;/code&gt; and private keys stops the everyday accidents. People have &lt;a href="https://www.theregister.com/2026/01/28/claude_code_ai_secrets_files/" rel="noopener noreferrer"&gt;watched agents read secret files&lt;/a&gt; and &lt;a href="https://www.docker.com/blog/coding-agent-horror-stories-the-rm-rf-incident/" rel="noopener noreferrer"&gt;delete home directories&lt;/a&gt;; those are mostly habits, and habits can be blocked.&lt;/p&gt;

&lt;p&gt;None of that is a sandbox. If &lt;code&gt;node&lt;/code&gt; or &lt;code&gt;python&lt;/code&gt; is allowed, a script can do anything your account can do, including deleting files. For a real wall, run the agent's tools in a container, under a separate low-privilege account, or in a VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where these notes come from
&lt;/h2&gt;

&lt;p&gt;I maintain an open-source MCP server, local-commander-mcp-agent (MIT), that packages fixes 1, 3 and part of 4 for agents; it was built around exactly these problems: &lt;a href="https://github.com/netbka/local-commander-mcp-agent" rel="noopener noreferrer"&gt;https://github.com/netbka/local-commander-mcp-agent&lt;/a&gt;. If you have a Windows setup that still trips your agent up (unusual paths, non-English usernames, locked-down corporate machines), I'd like to hear about it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article was drafted by an AI agent from my project notes and the threads linked above; every command, quote and link was checked before publishing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://localcmd.app/use-cases/coding-agents-on-windows" rel="noopener noreferrer"&gt;localcmd.app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windows</category>
      <category>ai</category>
      <category>powershell</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
