<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Anh Trinh]]></title><description><![CDATA[Software Developer - Making https://termrover.sh]]></description><link>https://ptgamr.substack.com</link><image><url>https://substackcdn.com/image/fetch/$s_!lCR0!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F942c76ba-2320-4a2b-b152-e540a7a37680_400x400.jpeg</url><title>Anh Trinh</title><link>https://ptgamr.substack.com</link></image><generator>Substack</generator><lastBuildDate>Wed, 07 Oct 2026 03:37:50 GMT</lastBuildDate><atom:link href="https://ptgamr.substack.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Anh Trinh]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[ptgamr@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[ptgamr@substack.com]]></itunes:email><itunes:name><![CDATA[Anh Trinh]]></itunes:name></itunes:owner><itunes:author><![CDATA[Anh Trinh]]></itunes:author><googleplay:owner><![CDATA[ptgamr@substack.com]]></googleplay:owner><googleplay:email><![CDATA[ptgamr@substack.com]]></googleplay:email><googleplay:author><![CDATA[Anh Trinh]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[A Pi setup with permission, sandbox, and auto-review]]></title><description><![CDATA[A secure-ish way to run Pi on your host.]]></description><link>https://ptgamr.substack.com/p/a-pi-setup-with-permission-sandbox</link><guid isPermaLink="false">https://ptgamr.substack.com/p/a-pi-setup-with-permission-sandbox</guid><dc:creator><![CDATA[Anh Trinh]]></dc:creator><pubDate>Thu, 13 Aug 2026 07:49:03 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Z1mt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>1. Motivation</h2><p>Pi is great precisely because it is so customizable. Its core stays small, and extensions let you assemble the tools and workflow that suit your environment.</p><p>The trade-off is that Pi is intentionally permissive. By default, it runs with the permissions of the user who launched it and does not provide a built-in approval system or OS sandbox. That &#8220;YOLO&#8221; experience is productive, but an agent capable of reading files and running arbitrary shell commands deserves a clearer security boundary.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Running Pi inside a container is one option. However, that becomes awkward when the project itself relies on Docker Compose, the host Docker daemon, or other host-only development services. Giving the container access to the Docker socket also hands it considerable power over the host.</p><p>What I wanted instead was:</p><ul><li><p>normal work inside the current repository;</p></li><li><p>OS-level isolation for ordinary shell commands;</p></li><li><p>deterministic blocks for secrets and paths outside the repository;</p></li><li><p>automatic review of requests that cross selected boundaries; and</p></li><li><p>a narrow, reviewed way to run specific commands on the host.</p></li></ul><p>Pi&#8217;s extensibility makes this possible, but discovering how the pieces fit together is not obvious. Hopefully this article gives you a useful starting point for a more secure host-based Pi setup.</p><h2>2. The setup</h2><p>The design combines three extensions:</p><ul><li><p><code>@gotgenes/pi-permission-system</code> defines deterministic <code>allow</code>, <code>ask</code>, and <code>deny</code> rules.</p></li><li><p><code>@erichll/pi-sandbox</code> runs ordinary Bash commands inside an OS sandbox.</p></li><li><p><code>@erichll/pi-auto-review</code> reviews network, permission, and host-execution requests that require a decision.</p></li></ul><p>Each layer has one job. The permission system establishes hard boundaries, the sandbox contains processes, and the reviewer handles contextual exceptions. An LLM decision never replaces a deterministic rule for something that must always be forbidden.</p><h3>How the piece fits together</h3><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Z1mt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Z1mt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Z1mt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png" width="1456" height="971" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:971,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1337559,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://ptgamr.substack.com/i/210997672?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Z1mt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 424w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 848w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 1272w, https://substackcdn.com/image/fetch/$s_!Z1mt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4ca1b94c-d82f-493a-bfc8-593b931a65a3_1536x1024.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h3>Install the components</h3><p>Assuming Node.js is already installed, install Pi and the Linux sandbox dependencies:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;b4d1bbde-65a6-4377-8015-c00572880910&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">npm install -g --ignore-scripts @earendil-works/pi-coding-agent
sudo apt-get install -y bubblewrap socat ripgrep</code></pre></div><p>Install the extensions globally, not from inside a project:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;9fc6e25a-9a33-4610-a90a-638226f35989&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">pi install npm:@gotgenes/pi-permission-system
pi install npm:@erichll/pi-auto-review
pi install npm:@erichll/pi-sandbox
pi list # to list the installed extensions</code></pre></div><p>These extensions are trusted host code, so review their source and record the versions used in a reproducible deployment.</p><h3>Define the hard boundaries</h3><p>Create <code>~/.pi/agent/extensions/pi-permission-system/config.json</code>:<br></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;9d7fd6ac-0724-492d-9dee-e7986eb8230d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "authorizerChain": ["pi-auto-review"],
  "permission": {
    "*": "allow",
    "path": {
      "*": "allow",
      "*.env": "deny",
      "*.env.*": "deny",
      "*.env.example": "allow",
      "*.pem": "deny",
      "*.key": "deny",
      "~/.ssh/*": "deny",
      "~/.aws/*": "deny"
    },
    "external_directory": "deny",
    "bash": {
      "*": "allow",
      "sudo *": "deny"
    }
  }
}</code></pre></div><p>The policy protects secret-shaped files, blocks access outside the working directory, and denies <code>sudo</code>. The <code>authorizerChain</code> connects permission requests that resolve to <code>ask</code> with the automatic reviewer.</p><p>The permissive Bash rule is intentional. It allows Bash calls to reach <code>pi-sandbox</code>; it does <strong>not</strong> run them directly on the host.</p><h3>Sandbox Bash and select host commands</h3><p>Create <code>~/.pi/agent/extensions/pi-sandbox/config.json</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;9e520ed9-c45a-401c-988e-3eb7b065ca0c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "subagents": {
    "provider": "builtin"
  },
  "hostIPC": {
    "mode": "ask",
    "preflightCommandPrefixes": [
      "docker compose ps",
      "docker compose up -d",
      "docker compose down",
      "docker compose exec app python manage.py test"
    ],
    "retryOnUnixSocketError": false
  },
  "filesystem": {
    "additionalAllowRead": []
  }
}</code></pre></div><p>Commands that do not match a <code>hostIPC</code> prefix run in the OS sandbox. Matching a prefix does not automatically authorize a command; it only makes the complete command eligible for review and one-shot host execution.</p><p>Prefixes should describe specific operations. For example:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;86360b42-e84a-4a33-8345-c43c0b435392&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">docker compose exec app python manage.py test</code></pre></div><p>is safer than:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;bash&quot;,&quot;nodeId&quot;:&quot;2c00eb2f-606a-496e-8105-8159bd9d25ec&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-bash">docker compose exec app</code></pre></div><p>The broader prefix would also make an interactive shell eligible for host execution. With <code>retryOnUnixSocketError</code> disabled, unlisted Docker commands remain sandboxed and fail when they try to reach the Docker socket.</p><p>Network access is handled similarly, with one important difference: approving a destination allows that specific connection while the command remains inside the sandbox.</p><h3>Configure automatic review</h3><p>Choose a model that is already configured and authenticated in Pi, then create <code>~/.pi/agent/extensions/pi-auto-review/config.json</code>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;json&quot;,&quot;nodeId&quot;:&quot;b30b1cf6-9344-4339-b5aa-b2dbde82cb48&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-json">{
  "model": "your-provider/your-review-model",
  "failureMode": "defer",
  "timeoutMs": 90000
}</code></pre></div><p><code>failureMode: "defer"</code> means a timeout, authentication error, or unavailable reviewer falls back to the human in an interactive session. It does not turn a successful model decision of <code>deny</code> into a prompt.</p><p>After a model denial, <code>/approve</code> lets an interactive user select a recent request and authorize one exact retry. The reviewer still evaluates that retry, and deterministic hard-deny rules remain final.</p><h2>3. Let the agents know about its environment (AGENTS.md)</h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;markdown&quot;,&quot;nodeId&quot;:&quot;913357d3-2ec9-4adf-bac4-5a46585d1e6e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-markdown"># Development environment
- You are operating inside a secure sandbox, where some commands might be blocked by the
  sandbox, and some others are allowed (whitelisted by the user) to be executed outside.
- If a command is blocked, do not try to find a workaround. Simply let the user know.
- You must run the tests for this project via docker
  - backend: `docker compose exec backend python manage.py test`.
  - frontend: `docker compose exec frontend npm run test`</code></pre></div><h2>4. Test the setup</h2><p>Before relying on the policy, test each boundary with non-sensitive fixtures:</p><ol><li><p><strong>Check normal project access.</strong> </p><p>Ask Pi: <em><strong>Run pwd and git status</strong></em><code>.</code></p><p>Expected: the commands run inside the sandbox.</p></li><li><p><strong>Check the sensitive-file policy.</strong> </p><p>Ask Pi: <em><strong>Read .env</strong></em><strong>.</strong></p><p>Expected: the deterministic path policy blocks the request.</p></li><li><p><strong>Check the repository boundary.</strong> </p><p>Ask Pi: <em><strong>Read ~/.config/i3/config</strong></em><strong>.</strong></p><p>Expected: the request is blocked because the path is outside the repository.</p></li><li><p><strong>Check the same boundary through Bash.</strong> </p><p>Ask Pi: <em><strong>Use bash to run: cat ~/.ssh/config</strong></em>.</p><p>Expected: the request is blocked before any sensitive data is returned.</p></li><li><p><strong>Check network review.</strong> </p><p>Ask Pi: <em><strong>Use curl to fetch https://some.untrusted.site</strong></em><code>.</code></p><p>Expected: the destination is reviewed before the sandbox connects to it.</p></li><li><p><strong>Check an eligible host command.</strong> </p><p>Ask Pi: <em><strong>Run docker compose ps</strong></em><code>.</code></p><p>Expected: the full command is reviewed and, if approved, runs once on the host.</p></li><li><p><strong>Check an unlisted Docker command.</strong> </p><p>Ask Pi: <em><strong>Run docker compose exec db bash</strong></em><code>.</code></p><p>Expected: the command remains sandboxed and cannot reach the Docker socket.</p></li></ol><p>A forbidden request may be stopped by the permission layer or the OS sandbox. That overlap is deliberate: the layers protect different execution paths.</p><p>After changing configuration, use <code>/reload</code> or restart Pi before testing again.</p><h2>5. Closing thoughts</h2><p>This setup is not a proof of safety. Extensions are trusted host code, automatic reviewers can make mistakes, and an approved host command really does execute with your user permissions. Docker itself is powerful, especially when containers have host mounts, credentials, privileged access, or the Docker socket.</p><p>The useful property here is separation of responsibility:</p><ul><li><p>deterministic policy handles things that must never happen;</p></li><li><p>the OS sandbox contains routine execution;</p></li><li><p>automatic review handles narrow, contextual decisions; and</p></li><li><p>host IPC exposes only the operations the development workflow actually needs.</p></li></ul><p>Pi&#8217;s permissive design is valuable. The goal is not to turn it into a sequence of approval dialogs, but to keep its freedom inside a meaningful boundary. For Docker-based projects, this arrangement preserves the convenience of running Pi on the host without making unrestricted host execution the default.</p><h1></h1><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[I found optimistic UI inside a terminal protocol]]></title><description><![CDATA[Learning about Mosh through the eyes of a frontend developer]]></description><link>https://ptgamr.substack.com/p/i-found-optimistic-ui-inside-a-terminal</link><guid isPermaLink="false">https://ptgamr.substack.com/p/i-found-optimistic-ui-inside-a-terminal</guid><dc:creator><![CDATA[Anh Trinh]]></dc:creator><pubDate>Mon, 20 Jul 2026 20:45:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!lCR0!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F942c76ba-2320-4a2b-b152-e540a7a37680_400x400.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am a Front-End developer, recently venturing into the Native App development (Thanks to AI) and I built TermRover (https://termrover.sh) because using other mobile terminals just really suck!</p><p>That&#8217;s when I learnt about Mosh, and found the idea surprisingly familiar to what I&#8217;ve been doing in the frontend: Optimistic UIs.</p><p>I am not sure why, but I&#8217;ve been obsessed with building Offline First webapp since forever, and I know that data syncing is the hardest part. It&#8217;s really refreshing when venturing out of my comfort zone, I found something similar.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p><p></p><h3>SSH waits for the server</h3><p>With SSH, a keypress travels to the server before the resulting character comes back to your screen.</p><p>On a fast network, you barely notice. On a high-latency cellular connection, typing can feel sticky because every character waits for a round trip.</p><p>This is similar to a frontend that waits for an API request before updating the interface.</p><p>Imagine tapping a favourite button and waiting half a second for it to change colour. The result may be correct, but the experience feels slow.</p><p>Frontend developers usually solve this with an optimistic update:</p><p>  1. Update the interface immediately.<br>  2. Send the action to the server.<br>  3. Reconcile with the server&#8217;s response.</p><p>The server remains the source of truth, but the interface does not need to feel slow.</p><h3>Mosh does something similar with typing</h3><p>When you type, Mosh predicts how the keystroke should appear and can display it locally without waiting for the server.</p><p>The real keystroke still travels to the remote host. When the authoritative terminal state returns, Mosh reconciles the prediction with it.</p><p><strong>Optimistic UI</strong></p><p>user action<br>    &#8594; update locally<br>    &#8594; send to server<br>    &#8594; reconcile</p><p><strong>Mosh predictive typing</strong></p><p>keypress<br>    &#8594; predict locally<br>    &#8594; send to remote terminal<br>    &#8594; reconcile</p><p>Mosh is not executing your command locally or pretending it succeeded. It is only predicting the visual effect of typing. The remote terminal remains authoritative.</p><p>That distinction made the idea click for me. Mosh treats the terminal as a distributed interface, not simply a pipe carrying bytes.</p><h3>The other benefit: the network can move</h3><p>Mosh uses SSH to authenticate and start mosh-server on the remote host. The live terminal traffic then moves over UDP.</p><p>Because the session is not tied to one TCP connection, your phone can change networks and continue talking to the same Mosh server.</p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;2295f4b9-e4e8-4870-af7b-867a3eb220e1&quot;,&quot;duration&quot;:null}"></div><p></p><p>In this recording, TermRover (https://termrover.sh) is connected through Mosh. I turn off Wi&#8209;Fi, the phone moves to 5G, and the same terminal remains ready for input.</p><p>No reconnect dialog. No new tab. No need to find the tmux session again.</p><h3>Bringing Mosh into TermRover</h3><p>I wanted Mosh to feel like an upgrade to SSH, not a separate workflow users needed to understand first.</p><p>In TermRover, you start with a normal SSH connection. Tap the SSH badge, choose Mosh, and the app switches the live tab while keeping your terminal and tmux context in place.</p><p>If the host does not have mosh-server, TermRover keeps the SSH connection open and guides you through installing it.</p><p>You can see the complete flow in the How to Mosh guide (https://termrover.sh/guide.html#how-to-mosh).</p><h3>A frontend idea in a terminal</h3><p>I added Mosh because I wanted TermRover to survive the move from Wi&#8209;Fi to cellular. What surprised me was how familiar its approach felt:</p><p>Predict locally. Keep the server authoritative. Reconcile when the real state arrives. That is optimistic UI, applied to one of our oldest interfaces. And it makes a remote terminal feel like it actually belongs on a phone.</p><p>Learn more about TermRover &#8594; (https://termrover.sh)</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://ptgamr.substack.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>