sketch: exclude internal processes (headless-chrome) from port monitoring

Add SKETCH_IGNORE_PORTS environment variable to headless-shell browser processes
and modify port monitoring to exclude processes with this variable.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: sff3b145df27ee3bek
diff --git a/loop/port_monitor.go b/loop/port_monitor.go
index 7eaa98f..14869da 100644
--- a/loop/port_monitor.go
+++ b/loop/port_monitor.go
@@ -4,7 +4,9 @@
 	"context"
 	"fmt"
 	"log/slog"
+	"os"
 	"sort"
+	"strings"
 	"sync"
 	"time"
 
@@ -178,6 +180,11 @@
 		return
 	}
 
+	// Skip processes with SKETCH_IGNORE_PORTS environment variable
+	if pm.shouldIgnoreProcess(port.Pid) {
+		return
+	}
+
 	// TODO: Structure this so that UI can display it more nicely.
 	content := fmt.Sprintf("Port %s: %s:%d", event, port.Proto, port.Port)
 	if port.Process != "" {
@@ -244,3 +251,28 @@
 	}
 	return removed
 }
+
+// shouldIgnoreProcess checks if a process should be ignored based on its environment variables.
+func (pm *PortMonitor) shouldIgnoreProcess(pid int) bool {
+	if pid <= 0 {
+		return false
+	}
+
+	// Read the process environment from /proc/[pid]/environ
+	envFile := fmt.Sprintf("/proc/%d/environ", pid)
+	envData, err := os.ReadFile(envFile)
+	if err != nil {
+		// If we can't read the environment, don't ignore the process
+		return false
+	}
+
+	// Parse the environment variables (null-separated)
+	envVars := strings.Split(string(envData), "\x00")
+	for _, envVar := range envVars {
+		if envVar == "SKETCH_IGNORE_PORTS=1" {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/loop/port_monitor_test.go b/loop/port_monitor_test.go
index 1339720..ec847bd 100644
--- a/loop/port_monitor_test.go
+++ b/loop/port_monitor_test.go
@@ -2,6 +2,8 @@
 
 import (
 	"context"
+	"os"
+	"os/exec"
 	"testing"
 	"time"
 
@@ -224,6 +226,42 @@
 	}
 }
 
+// TestPortMonitor_ShouldIgnoreProcess tests the shouldIgnoreProcess function.
+func TestPortMonitor_ShouldIgnoreProcess(t *testing.T) {
+	agent := createTestAgent(t)
+	pm := NewPortMonitor(agent, 100*time.Millisecond)
+
+	// Test with current process (should not be ignored)
+	currentPid := os.Getpid()
+	if pm.shouldIgnoreProcess(currentPid) {
+		t.Errorf("current process should not be ignored")
+	}
+
+	// Test with invalid PID
+	if pm.shouldIgnoreProcess(0) {
+		t.Errorf("invalid PID should not be ignored")
+	}
+	if pm.shouldIgnoreProcess(-1) {
+		t.Errorf("negative PID should not be ignored")
+	}
+
+	// Test with a process that has SKETCH_IGNORE_PORTS=1
+	cmd := exec.Command("sleep", "5")
+	cmd.Env = append(os.Environ(), "SKETCH_IGNORE_PORTS=1")
+	err := cmd.Start()
+	if err != nil {
+		t.Fatalf("failed to start test process: %v", err)
+	}
+	defer cmd.Process.Kill()
+
+	// Allow a moment for the process to start
+	time.Sleep(100 * time.Millisecond)
+
+	if !pm.shouldIgnoreProcess(cmd.Process.Pid) {
+		t.Errorf("process with SKETCH_IGNORE_PORTS=1 should be ignored")
+	}
+}
+
 // createTestAgent creates a minimal test agent for testing.
 func createTestAgent(t *testing.T) *Agent {
 	// Create a minimal agent for testing