[object Object]

← back to Slack Idea Board

Idea-board: localhost-only spawn guard (public tunnel previews/copies, never spawns)

fac3b236a8406c8b160856a52555b99d1fb5ce21 · 2026-08-06 17:01:07 -0700 · Steve Abrams

Files touched

Diff

commit fac3b236a8406c8b160856a52555b99d1fb5ce21
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 17:01:07 2026 -0700

    Idea-board: localhost-only spawn guard (public tunnel previews/copies, never spawns)
---
 public/index.html | 3 ++-
 server.js         | 9 +++++++++
 2 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/public/index.html b/public/index.html
index 89ee279..8662a7e 100644
--- a/public/index.html
+++ b/public/index.html
@@ -160,7 +160,8 @@ async function launchAction(id, step, label, btn){
       if(Date.now() + RETRY_MS > deadline) break;    // give up → falls through to the throttle toast
       for(let s=Math.round(RETRY_MS/1000); s>0; s--){ setLbl(`throttled · retry ${s}s`); await sleep(1000); }
     }
-    if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new iTerm2 tab<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
+    if(j.launched){ toast(`🚀 <b>${esc(label||step)}</b> launched in a new iTerm2 window<br><code>${esc((j.prompt||'').slice(0,160))}…</code>`); }
+    else if(j.localOnly){ toast('🔒 '+esc(j.reason||'spawn is localhost-only')+'<br>Tip: Shift-click to copy the command instead.'); }
     else if(j.throttled){ toast('⏳ Still throttled after retries — '+esc(j.reason||'try again shortly')); }
     else if(j.needsConfirm){ toast('Launch blocked — server requires confirm'); }
     else { toast('Launch failed: '+esc(j.error||'unknown')); }
diff --git a/server.js b/server.js
index 741df64..efef8e0 100644
--- a/server.js
+++ b/server.js
@@ -232,6 +232,15 @@ const server = http.createServer(async (req, res) => {
         res.writeHead(200, { 'Content-Type': 'application/json' });
         return res.end(JSON.stringify({ launched: false, dryRun: !!j.dryRun, needsConfirm: !j.dryRun, step, cwd, prompt }));
       }
+      // Guard 0 — spawn is LOCALHOST-ONLY. The board is exposed to the internet via a Cloudflare
+      // tunnel (basic-auth only), so a real `claude --model opus` terminal spawn on this Mac must
+      // never be triggerable from the public URL. Preview/copy still works everywhere; only the
+      // actual spawn is gated. The tunnel is identified by the proxy headers Cloudflare injects.
+      const viaProxy = !!(req.headers['cf-ray'] || req.headers['x-forwarded-for'] || req.headers['x-forwarded-host']);
+      if (viaProxy) {
+        res.writeHead(403, { 'Content-Type': 'application/json' });
+        return res.end(JSON.stringify({ launched: false, localOnly: true, reason: 'spawn is localhost-only — open http://localhost:9820 on the Mac to launch', step }));
+      }
       const key = (j.id || '') + '|' + step;
       const now = Date.now();
       // Guard 2 — per-idea in-flight + cooldown dedupe: drop a repeat of the same idea within the cooldown.

← 0638b1f Idea-board: launch in a NEW iTerm2 window (not a tab); lift  ·  back to Slack Idea Board  ·  creds-safe fetch guard: resolve relative fetch vs credential 48fce21 →