Help us improve
Share bugs, ideas, or general feedback.
From loom
Use when building a web application that needs Claude Code CLI (`claude -p`) or the Agent SDK as its backend runtime — a server that spawns Claude processes to power a custom browser interface. Triggers: "build an app that uses Claude", "Claude as backend/runtime", "Claude-powered web app", "wrap claude -p in a server", "streaming Claude output to browser", or any web app needing Claude's agentic capabilities through a purpose-built interface. NOT for direct Anthropic API usage, simple chat replicas, or desktop apps (use loom-desktop).
npx claudepluginhub popmechanic/loomHow this skill is triggered — by the user, by Claude, or both
Slash command
/loom:loomThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You're helping someone build an application where Claude Code is the runtime —
Builds LLM-powered applications with Claude API, Anthropic SDKs, or Agent SDK. Detects project language (Python, TypeScript, Java, Go, Ruby, C#, PHP) and provides language-specific docs and defaults.
Provides instructions for building LLM-powered apps with the Claude API or Anthropic SDK, including language detection and code examples for Python, TypeScript, Java, Go, Ruby, and more.
Builds, debugs, and optimizes Claude API / Anthropic SDK apps with prompt caching, model migration, and feature tuning.
Share bugs, ideas, or general feedback.
You're helping someone build an application where Claude Code is the runtime —
not a helper writing React components, but the actual engine that powers
the application's intelligence. The interface talks to a server that spawns
claude -p processes or uses the Agent SDK, streaming results back to the browser.
This is a different posture than normal web development. Normally, the backend is a database and some business logic. Here, the backend is Claude — an agent that can read files, run commands, search code, and reason about complex tasks. The interface's job is to give that agent a form — to decide what the output looks like and how someone interacts with it.
Most "AI-powered" web apps just wrap a chat API. They put a text box on screen, send messages to an LLM, and show the response. That's a chat replica.
Claude Code is not a chat API. It's an agentic runtime with filesystem access, tool use, multi-turn sessions, structured output, and streaming. Building an interface on top of Claude Code means designing something that exposes these capabilities in ways that make sense — not just conversations, but whatever interaction paradigm fits what the person is trying to do.
The question to help users explore is: what would you build if your backend could read files, run code, search the web, coordinate multiple agents, and stream its reasoning to the browser in real time?
Every Loom app has the same basic shape:
┌──────────────┐ HTTP/WS ┌──────────────┐ stdio/SDK ┌──────────────┐
│ Interface │ ◄──────────────► │ Node Server │ ◄────────────► │ claude -p │
│ (React/HTML)│ │ (Express/etc) │ │ (Agent SDK) │
└──────────────┘ └──────────────┘ └──────────────┘
UI layer Bridge layer Runtime layer
Interface: The custom UI. Whatever makes sense for what's being built.
Node Server: The bridge. Receives requests from the interface, spawns Claude
processes, parses output, streams results back. This is where you handle
Anthropic OAuth authentication, rate limiting, session management, and the
mapping between web concepts and Claude invocations. The server verifies
credentials exist before spawning Claude and shows a setup screen if they're
missing (see references/oauth-reference.md).
Claude Runtime: The intelligence. claude -p with the right flags, or the
Agent SDK for programmatic control. This is where the agentic work happens —
reading files, running commands, generating structured output. Before the
runtime can start, the user needs valid Anthropic credentials. See the OAuth
setup in the Building It section below.
| Pattern | When to Use | How |
|---|---|---|
| REST + JSON | One-shot requests, data extraction | POST → claude -p --output-format json → JSON response |
| SSE (Server-Sent Events) | Streaming text to browser | claude -p --output-format stream-json --verbose → SSE stream |
| WebSocket | Bidirectional, multi-turn sessions | WS connection → claude -p --input-format stream-json |
| Background job | Long-running tasks | Queue → claude -p process → poll for result or push via WS |
| HTTP Hooks | Tool visibility, permission approval, lifecycle events | Configure in .claude/settings.local.json, server receives POSTs at lifecycle points |
For most apps, start with REST + SSE: REST for triggering tasks, SSE for
streaming progress and results. Add WebSockets only if you need true
bidirectional communication (e.g., the user can interrupt or steer while
Claude is working). Add HTTP Hooks when you need structured tool-lifecycle
events or UI-driven permission approval (see references/advanced-patterns.md#http-hooks).
When someone comes to you with an idea, walk through these design questions. Don't dump them all at once — have a natural conversation. But cover this ground before you start building:
Get concrete about the interface, not the AI. What's the layout? What does someone click? What appears when Claude is working? What does the final output look like?
Ask: "Walk me through the screen. What does someone see when they open this? What do they do first? What happens next?"
How does the person's action translate to a Claude invocation?
| Interaction | Claude Pattern |
|---|---|
| Click a button, get a result | One-shot claude -p, REST response |
| Watch progress in real-time | Stream-JSON → SSE to browser |
| Multi-step workflow with state | Session-based (--session-id first turn, --resume <id> after) |
| Concurrent analysis of multiple items | Parallel claude -p processes |
| Person steers while Claude works | Bidirectional streaming via WebSocket |
Map each action to what Claude needs behind the scenes:
Read,Glob,Grep) vs. modification (Write,Edit,Bash)
vs. no tools at all (--tools "" for pure reasoning)--system-prompt "You are..." replaces the default system prompt
entirely — use this for character personas, branded assistants, or any app where
Claude should NOT inherit the user's CLAUDE.md settings. --append-system-prompt
adds to the default prompt (preserving user settings, skills, etc.) — use this
when Claude should still act as a general assistant with extra instructions.references/server-patterns.md#handling-file-uploads-and-drops).--json-schema for structured + result for narrative)?This is where custom interfaces shine over CLIs. You can render Claude's structured output as rich UI:
Design the JSON schema to match the UI components you want to render. The schema IS your API contract between Claude and the frontend.
Web apps add security concerns that CLIs don't have:
references/oauth-reference.md).
For multi-user apps, each user gets their own server-side session with tokens stored
in memory. The server injects CLAUDE_CODE_OAUTH_TOKEN into each spawned Claude
process — no shared credential file. This ensures each user's Claude processes
use only their own Anthropic subscription.--permission-mode and --allowedTools that work.
Prefer dontAsk (auto-denies unallowed tools) over bypassPermissions (skips all checks).
Only use bypassPermissions when --allowedTools fully constrains Claude's capabilities
and you need unattended execution in a trusted environment (CI/CD, local dev tools).references/oauth-reference.md). Each user authenticates independently,
gets a session cookie, and their Claude processes receive their own token via env var.
Consider separate working directories per user if they have persistent file operations.| Need | Model | Why |
|---|---|---|
| Fast responses (<3s) | haiku | Classification, extraction, routing |
| Good quality, reasonable speed | sonnet | Default for most apps |
| Best reasoning | opus | Complex analysis, code generation |
| Reliability | --fallback-model haiku | Auto-fallback on overload |
For web UIs, perceived speed matters. Use streaming to show partial results immediately, even when using slower models.
Default to Node.js/TypeScript with Express for the server and plain HTML/CSS/JS or React for the frontend, unless the person prefers otherwise.
Before any server pattern works, each user needs valid Anthropic credentials. For multi-user apps, tokens are stored in an in-memory session store — NOT in a shared file. Your server should:
requireAuth middleware on protected endpointshttps://api.anthropic.com/v1/me
using the new access_token as a Bearer token, and store {name, email} in the
session. Without this, the frontend can't show who's logged in — it falls back to
a generic "CONNECTED" label. This is the most commonly omitted step.refreshSessionIfNeeded() before spawning Claude processesCLAUDE_CODE_OAUTH_TOKEN env var on each spawnAlways log OAuth errors server-side. The exchange endpoint calls
Anthropic's token endpoint over HTTPS — this is the most failure-prone
path (DNS issues on fresh VMs, transient network errors, expired codes).
Both the !resp.ok branch and the catch block must console.error
the actual error, not silently return a generic message. Without this,
journalctl shows nothing when the exchange fails and you're debugging
blind.
Read
references/oauth-reference.mdfor the complete implementation — PKCE utilities, server endpoints, session store,requireAuthmiddleware, token refresh, and the ready-to-use React<SetupScreen>component.
The server's job is simple: receive HTTP requests, spawn Claude, return results.
Read references/cli-runtime-reference.md for the full claude -p flag reference.
Every pattern runs Claude in a server — no human sitting at a terminal to approve tool use. Three flags are non-negotiable:
--permission-mode dontAsk — In a server context, there's nobody to click
"approve." Without this flag, Claude hangs forever waiting for interactive
input. dontAsk auto-denies any tool not in --allowedTools, which is exactly
what you want: predictable, unattended execution.
Critical: Pair --permission-mode dontAsk with --allowedTools or --tools.
Without allowed tools, dontAsk gives Claude no tools at all — it can reason
but can't act, and the failure is silent (no error, just missing results).
--max-turns — Prevents
conversational loops where Claude keeps trying approaches that won't work.
5 for one-shot tasks, 10-15 for streaming, 20 for multi-turn sessions.
Every pattern also handles three failure modes:
execFileSync throws; spawn emits a close event.parsed.is_error before
using structured_output.Three helpers used by every pattern: cleanEnv() (remove nesting guards),
createStreamParser() (buffer stdout into JSON lines), and
spawnEnvForUser() (inject OAuth token into spawn env).
See
references/server-patterns.md#shared-utilitiesfor the full implementations with explanatory prose.
| Pattern | When to Use | Reference |
|---|---|---|
| REST + JSON | One-shot requests, data extraction | references/server-patterns.md#pattern-rest-endpoint |
| SSE Streaming | Streaming text to browser | references/server-patterns.md#pattern-sse-streaming |
| WebSocket | Bidirectional, multi-turn | references/server-patterns.md#pattern-websocket-session |
| Background Job | Long-running tasks | references/server-patterns.md#pattern-background-job-with-progress |
| Parallel | Batch analysis | references/server-patterns.md#pattern-parallel-analysis |
| Structured Extraction | Fast async data extraction (Haiku) | references/advanced-patterns.md#structured-extraction-async-haiku |
| Persistent Session | Long-lived process, lower latency | references/advanced-patterns.md#persistent-session-long-lived-process |
| Action Markers | Mid-stream structured events | references/advanced-patterns.md#action-markers |
| HTTP Hooks | Tool lifecycle events, browser permission approval | references/advanced-patterns.md#http-hooks |
Read
references/server-patterns.mdwhen implementing a specific server endpoint or wiring up the frontend. Readreferences/advanced-patterns.mdwhen the basic patterns aren't enough for your use case.
When using --output-format stream-json --verbose --include-partial-messages,
Claude emits newline-delimited JSON events:
| Event Type | Shape | Forward? |
|---|---|---|
system | {type:"system", subtype:"init", session_id, model, tools} | Optional (extract session_id) |
stream_event | {type:"stream_event", event:{delta:{text:"..."}}} | Yes (live text) |
assistant | {type:"assistant", message:{content:[...]}} | Tool use only (text already streamed) |
tool_result | {type:"tool_result", tool_name, content, is_error} | Optional |
compact | {type:"compact"} | No (internal) |
rate_limit_event | {type:"rate_limit_event", rate_limit_info:{...}} | No (log it) |
result | `{type:"result", subtype:"success" | "error_max_turns", is_error}` |
See
references/server-patterns.md#stream-json-event-typesfor complete notes, extended thinking behavior, code samples for extracting text/tool use, and max-turns detection.
Use fetch() + ReadableStream for POST-based SSE (CSRF-safe). Parse data:
lines, dispatch on event type (token, done, error). For quick prototyping,
EventSource works for GET-based SSE.
See
references/server-patterns.md#frontend-integrationfor the complete streaming text display and structured result rendering code.
Every pattern in references/server-patterns.md handles errors inline — you
won't find a separate error handling block to copy-paste because it doesn't
belong in one. Here's the mental model behind the three failure modes:
stderr fires first. Claude writes diagnostics, warnings, and model errors
here before the process exits. Always pipe it somewhere — console.error at
minimum. In production, send it to your logging stack. This is your primary
debugging signal when a request fails silently.
Non-zero exit codes mean Claude didn't complete successfully. Common
causes: model overloaded (503 from upstream), permission denied (tool not
in --allowedTools), or process killed
by your timeout. For execFileSync, this throws — catch it. For spawn,
listen on the close event and check the code.
Malformed output happens when a process is killed mid-stream (timeout,
client disconnect, OOM). The stdout buffer contains partial JSON that won't
parse. Always wrap JSON.parse in try/catch, and always check parsed.is_error
before reaching for structured_output — Claude sets this flag when it
couldn't complete the task (tool failures, turn limit exceeded, etc.).
See
references/server-patterns.md#error-surfacing-checklistfor the three-channel checklist with code samples.
When you build the app, produce:
server.ts — Express server using the Express baseline from
references/server-patterns.md#server-setup
(express, cookie-parser, express.static). Includes session store,
requireAuth middleware, OAuth endpoints (/api/oauth/start,
/api/oauth/exchange, /api/health, /api/logout), and your app's
endpoint pattern(s). The exchange endpoint must fetch the user's
profile from https://api.anthropic.com/v1/me and store it in
the session. Each spawn uses spawnEnvForUser() to inject the
requesting user's token.public/index.html — The frontend, starting with the <SetupScreen>
component (shown when no session exists) and your app's main UI
(shown after authentication). Include a user indicator showing
the user's email (from /api/health → user.email) and a logout
button that calls POST /api/logout. Use a fallback label like
"CONNECTED" if the profile has no email. Protected endpoints check
for 401 responses and redirect to the setup screen (in-memory
sessions are wiped on server restart). If the app has both a chat
input and a setup screen input, use input.setup-input selectors
(not .setup-input) to avoid CSS specificity conflicts with global
input[type="text"] rules — see references/oauth-reference.md.package.json — Dependencies (express, cookie-parser, uuid,
plus ws and cookie if using WebSockets, and cors if frontend/server
are separate origins) and a start scriptThese are the silent-failure modes — things that break with NO error message.
The patterns in references/server-patterns.md demonstrate correct handling
for each, but they're easy to miss or deviate from when generating a new app.
--include-partial-messages is on every streaming spawn — without it, text dumps as a single block instead of streaming token-by-tokenstream_event only, NOT from assistant text blocks — otherwise every token appears twicespawnEnvForUser() is called on every spawn/execFileSync — this removes nesting guards AND injects the user's OAuth token; bare cleanEnv() omits the token and causes silent auth failure--permission-mode dontAsk is paired with --allowedTools or --tools — without allowed tools, Claude produces an empty result with NO errorsubtype === "error_max_turns" is checked on result events — this fires with is_error: false, so unchecked it looks like successexpress.json() middleware is applied before any route that reads req.body — without it, req.body is undefined and the spawn gets an empty promptcookie-parser middleware is applied before any route that reads req.cookies — without it, requireAuth sees undefined and every request returns 401/api/oauth/exchange handler after token exchange — without it, the frontend shows "CONNECTED" instead of the user's email/api/health does NOT use requireAuth — it must return {needsSetup: true} for unauthenticated users, not 401req.headers.cookie — Express middleware does not run on WebSocket handshakesFor simple apps, a single server.ts serving a static index.html is ideal.
For complex UIs, scaffold a React frontend with a separate server.
After generating, offer to start the server and open it in the browser together. Then iterate based on what the person sees.
When deploying to a remote VM (exe.dev, etc.), verify outbound connectivity after starting the service. Fresh VMs can have transient DNS or network issues that cause the first OAuth exchange to fail:
# Verify the app can reach Anthropic's token endpoint
curl -s -o /dev/null -w "%{http_code}" https://platform.claude.com/v1/oauth/token
# Should return 405 (Method Not Allowed for GET) — confirms connectivity
If this returns a network error, wait a few seconds and retry. Don't declare deployment complete until the VM can reach Anthropic's servers.
The most interesting Loom apps are not chat interfaces in a browser. They're things that couldn't exist without an agentic runtime — applications where the backend can read, reason, and act on context that traditional APIs can't access.
Help people think about what they actually want to make. The question isn't "how do I put a chat box in a browser?" but "what would this look like if there were an intelligence behind it?"