diff --git a/playground/README.md b/playground/README.md
new file mode 100644
index 0000000..5cc7e8c
--- /dev/null
+++ b/playground/README.md
@@ -0,0 +1,48 @@
+# claude_wire — chat web (playground)
+
+Chat en el navegador sobre `claude_wire`: el texto de la respuesta se obtiene
+interceptando el SSE del modelo en la red (no parseando el render de la terminal).
+
+Backend Go con SSE (`web/server.go`) que, por cada mensaje, lanza `claude_wire`
+y reenvía su NDJSON (`text_delta` + `result`) al navegador. Frontend vanilla
+(`web/index.html`), sin frameworks ni node_modules. No se indexa (playground del
+padre).
+
+## Prerrequisitos
+
+- `mitmproxy` instalado (`mitmdump` en el PATH) + CA generada
+ (`~/.mitmproxy/mitmproxy-ca-cert.pem`).
+- Binarios compilados:
+ - `apps/claude_pipe/claude_pipe` (driver de la TUI).
+ - `apps/claude_wire/claude_wire` (runner que intercepta el SSE).
+
+```bash
+cd apps/claude_pipe && CGO_ENABLED=1 go build -tags fts5 -o claude_pipe .
+cd ../claude_wire && go build -o claude_wire .
+```
+
+## Lanzar
+
+```bash
+cd apps/claude_wire/playground
+go run ./web # http://localhost:8100
+# o con flags:
+go run ./web --port 8100 --root /home/enmanuel/fn_registry --wire ../claude_wire
+```
+
+Abre `http://localhost:8100` y escribe. Cada mensaje dirige la TUI interactiva de
+claude por un mitmproxy y lee la respuesta del SSE de `api.anthropic.com`, token a
+token, exacta. Corta en `message_stop` (sin idle ciego): ~9s por respuesta.
+
+## vs el chat de claude_pipe
+
+| | `claude_pipe` chat (`:8099`) | `claude_wire` chat (`:8100`) |
+|---|---|---|
+| Texto | parseado del render (heurístico) | exacto del SSE de la red |
+| Streaming | snapshots del grid | token real |
+| Latencia | ~15s | ~9s |
+
+## Notas
+
+- Mensajes serializados (un `claude_wire` a la vez: usa un puerto mitmproxy fijo).
+- Sin memoria entre turnos (cada mensaje es una sesión claude one-shot).
diff --git a/playground/web/go.mod b/playground/web/go.mod
new file mode 100644
index 0000000..f856045
--- /dev/null
+++ b/playground/web/go.mod
@@ -0,0 +1,3 @@
+module cw_web
+
+go 1.25.0
diff --git a/playground/web/index.html b/playground/web/index.html
new file mode 100644
index 0000000..0e11e3a
--- /dev/null
+++ b/playground/web/index.html
@@ -0,0 +1,159 @@
+
+
+
+
+
+claude_wire · chat (SSE interceptado)
+
+
+
+
+
+ claude_wire · chat
+ texto exacto del modelo, interceptado del SSE de la red
+
+
+
+
+
sistema
Escribe un mensaje. Cada turno dirige la TUI interactiva de claude por un mitmproxy y lee la respuesta DEL CABLE (el SSE de api.anthropic.com), no de la pantalla. Texto exacto, token a token, sin memoria entre mensajes.
+
+
+
+
+ Cada mensaje = captura del SSE via mitmproxy. Corta en message_stop (sin idle ciego). ~9s por respuesta; el grueso es el arranque de la TUI.
+
+
+
+
diff --git a/playground/web/server.go b/playground/web/server.go
new file mode 100644
index 0000000..3196a1a
--- /dev/null
+++ b/playground/web/server.go
@@ -0,0 +1,123 @@
+// Command server is a browser chat over claude_wire — the network-intercept path.
+// For each message it launches claude_wire, which drives the interactive claude TUI
+// through a mitmproxy and reads the model's SSE off the wire, then relays the
+// resulting NDJSON (text_delta + result) to the browser as Server-Sent Events.
+//
+// Versus the claude_pipe chat (which parses the terminal render), this one shows the
+// exact model text token by token and finishes as soon as the model's message_stop
+// arrives — no blind idle wait. ~9s vs ~15s per message.
+//
+// claude_wire uses a fixed mitmproxy port, so messages are serialized with a mutex
+// (a chat is sequential anyway: you wait for the reply before sending the next).
+//
+// Each message is an independent one-shot claude session (no memory across turns).
+//
+// Run:
+//
+// cd apps/claude_pipe && CGO_ENABLED=1 go build -tags fts5 -o claude_pipe . # the TUI driver
+// cd ../claude_wire && go build -o claude_wire . # the runner
+// cd playground && go run ./web # http://localhost:8100
+package main
+
+import (
+ "bufio"
+ _ "embed"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sync"
+)
+
+//go:embed index.html
+var indexHTML []byte
+
+var (
+ wireBin string
+ root string
+ one sync.Mutex // serialize: one claude_wire at a time (fixed proxy port)
+)
+
+func main() {
+ port := flag.String("port", "8100", "port to listen on")
+ bin := flag.String("wire", "/home/enmanuel/fn_registry/apps/claude_wire/claude_wire", "path to the claude_wire binary")
+ rootDir := flag.String("root", "/home/enmanuel/fn_registry", "cwd for claude (MCP-approved repo)")
+ flag.Parse()
+
+ abs, err := filepath.Abs(*bin)
+ if err != nil {
+ log.Fatalf("resolve --wire: %v", err)
+ }
+ if _, err := os.Stat(abs); err != nil {
+ log.Fatalf("claude_wire binary not found at %s — build it first:\n (cd .. && go build -o claude_wire .)", abs)
+ }
+ wireBin, root = abs, *rootDir
+
+ http.HandleFunc("/", handleIndex)
+ http.HandleFunc("/chat", handleChat)
+
+ log.Printf("claude_wire web chat → http://localhost:%s (wire=%s root=%s)", *port, wireBin, root)
+ log.Fatal(http.ListenAndServe(":"+*port, nil))
+}
+
+func handleIndex(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _, _ = w.Write(indexHTML)
+}
+
+// handleChat runs claude_wire for the prompt and relays its NDJSON as SSE.
+func handleChat(w http.ResponseWriter, r *http.Request) {
+ prompt := r.URL.Query().Get("prompt")
+ if prompt == "" {
+ http.Error(w, "missing prompt", http.StatusBadRequest)
+ return
+ }
+ flusher, ok := w.(http.Flusher)
+ if !ok {
+ http.Error(w, "streaming unsupported", http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "text/event-stream")
+ w.Header().Set("Cache-Control", "no-cache")
+ w.Header().Set("Connection", "keep-alive")
+ w.Header().Set("X-Accel-Buffering", "no")
+
+ // Serialize: claude_wire binds a fixed mitmproxy port.
+ one.Lock()
+ defer one.Unlock()
+
+ cmd := exec.CommandContext(r.Context(), wireBin, "--prompt", prompt, "--cwd", root)
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ sse(w, flusher, "error", fmt.Sprintf(`{"message":%q}`, err.Error()))
+ return
+ }
+ cmd.Stderr = os.Stderr
+ if err := cmd.Start(); err != nil {
+ sse(w, flusher, "error", fmt.Sprintf(`{"message":%q}`, err.Error()))
+ return
+ }
+
+ sc := bufio.NewScanner(stdout)
+ sc.Buffer(make([]byte, 1024*1024), 1024*1024)
+ for sc.Scan() {
+ sse(w, flusher, "", sc.Text())
+ }
+ _ = cmd.Wait()
+ sse(w, flusher, "done", "{}")
+}
+
+func sse(w http.ResponseWriter, f http.Flusher, event, data string) {
+ if event != "" {
+ fmt.Fprintf(w, "event: %s\n", event)
+ }
+ fmt.Fprintf(w, "data: %s\n\n", data)
+ f.Flush()
+}