A document-style interface for the Claude Code CLI. You type a paragraph, the model replies as the next paragraph, and the work it does along the way shows up as margin comments and revisions — like a document editor, not a chat window or a terminal.
Most AI coding frontends look like a chat app or a terminal: bubbles, avatars, "user / assistant" labels, a model picker, Send/Stop buttons, a scrolling log. DocShell takes a different stance — a calm, document-shaped surface:
- Your message is just a paragraph in the document body.
- The model's reply is the next paragraph.
- Tool calls and the process in between become margin comments (a one-line summary you can expand) and inline revisions (strikethrough + additions for file edits).
The middle process is visible but unobtrusive — you can see what the agent read, ran, and changed, without the page turning into a white-background terminal log.
- Document-style UI — no chat bubbles, avatars, role labels, sidebar, model selector, or New Chat / Send / Stop buttons. Input and output are both document paragraphs.
- Process visibility — tool calls land in the right-hand margin as collapsible comments (
Read 2 files,ran ls, …); file edits render as inline revisions. - Real Claude Code backend, your subscription — spawns the local
claudeCLI over its OAuth login (your Claude Code subscription), not the API/SDK. Claude Code is the only backend currently implemented. - Clean multi-turn — one persistent
claude --input-format stream-jsonprocess per document; context stays continuous across turns. Survives server restarts via--resume. - Multiple documents — create / switch between docs; each is its own conversation, persisted locally (IndexedDB). Refresh starts a fresh conversation; old docs remain in the File menu.
- Type-ahead queue — keep typing while a reply streams; follow-ups queue and send automatically.
- Esc to interrupt — stop a generation mid-stream (the Ctrl-C you don't have in a doc).
- Safety handbrake — a
PreToolUsehook hard-blocks catastrophic Bash (rmof root/home/system dirs,mkfs,ddto block devices, fork bombs,shred). It's a handbrake, not a sandbox (see Security). - Token auth for network deploy — a non-loopback bind requires a shared token of at least 32 characters, so you can reach it from another device over a private network without leaving the agent API open.
Browser (document UI) Server (Next.js, your machine)
┌───────────────────────┐ ┌─────────────────────────────────┐
│ paragraphs (in/out) │◄──SSE──►│ /api/chat │
│ margin comments │ │ └─ lib/cc-process.ts │
│ IndexedDB (docs) │ │ per-doc persistent process │──► claude (stream-json)
└───────────────────────┘ │ lib/stream-parser.ts │
│ lib/tool-comment.ts │
└─────────────────────────────────┘
lib/cc-process.tskeeps one persistentclaude --input-format stream-jsonprocess per document and feeds each turn as a structured message over stdin. This gives clean multi-turn memory and avoids the synthetic "Continue from where you left off" turn that--resumeinjects. After a server restart it recovers a document's context with--resume <sessionId>(swallowing the synthetic recovery turn).lib/stream-parser.tsparses the CLI'sstream-jsoninto chunks (text deltas, tool_use, tool_result, result, errors).lib/tool-comment.tsshapes tool calls into margin comments / revisions.- The frontend (
app/page.tsx) renders everything as a document and streams replies over SSE.
Prerequisites:
- Node.js 20+
- Claude Code CLI installed and logged in (
claudeon yourPATH)
npm install
npm run dev # http://127.0.0.1:3000The guarded launcher binds to 127.0.0.1, so no token is required (same-machine only). Open the page and start typing. Direct npx next ... launches do not create the local-only auth attestation and are intentionally rejected by the API when no token is set.
scripts/start-prod.sh and scripts/run-server.sh default to 127.0.0.1. To reach DocShell from another device on a private network, request a network bind explicitly and set a token. Every supported launcher fails closed if a non-loopback address is used without DOCSHELL_TOKEN:
npm run token:init
DOCSHELL_HOST=0.0.0.0 PORT=3010 bash scripts/start-prod.shThen open http://<server-ip>:<port>/#token=<your-token> once; the token is stored in the browser afterward. The token is accepted only from the URL fragment, which browsers never send to the server, so it stays out of access logs; ?token= query parameters are intentionally not supported.
On macOS, run it via a launchd LaunchAgent in the GUI session (scripts/run-server.sh) rather than a bare SSH/background process — otherwise the claude CLI can't read the login Keychain where its subscription credentials live, and you'll see Not logged in.
| Env var | Default | Meaning |
|---|---|---|
DOCSHELL_TOKEN |
(unset) | If set, every API request must carry a matching x-docshell-token header. A value of at least 32 characters is required for every non-loopback bind. |
DOCSHELL_HOST |
127.0.0.1 |
Bind address. Without a token, only literal 127.0.0.1 or ::1 is accepted. |
PORT |
3010 (prod) |
Server port. |
DOCSHELL_NO_REMOTE_CONTROL |
(unset) | Set to 1 to disable Claude's Remote Control on spawned sessions. |
DOCSHELL_MAX_PROCESSES |
8 |
Maximum resident Claude processes. New turns evict the least-recently-used idle process; active streams are never evicted. Valid range: 1–64. |
DOCSHELL_LOG_DIR |
$XDG_STATE_HOME/docshell/logs or ~/.local/state/docshell/logs |
Private directory for unique mode-600 logs created by start-prod.sh. |
The backend is currently Claude Code and the model is currently fixed to opus. Effort is chosen in the document's settings panel (Standard / Deep / Fast → high / max / low); the default is max.
DocShell runs the agent with --permission-mode bypassPermissions (tools auto-execute) plus a destructive-command guard. This is a handbrake, not a sandbox:
- The guard only blocks a small set of blatant, irreversible commands; it can be bypassed by obfuscation (base64-pipe-to-shell, child interpreters, etc.).
- The working directory is the user's
HOMEby default. - Server-only
DOCSHELL_*authentication and bind variables are removed from the Claude child process environment, but the agent still runs as the same OS user and can access that user's permitted files. Use a separate account or stronger isolation for untrusted content. - Uploaded temporary files are kept in a user-owned mode-
700directory as mode-600files and are removed after each turn; a hard crash can still leave temporary files behind.
For anything beyond personal local use, isolate properly (container / restricted account / read-only mounts / scoped cwd), and keep DOCSHELL_TOKEN set whenever the server is reachable over a network. Token-in-custom-header is CSRF-resistant.
In no-token mode, Next.js does not expose a trustworthy peer socket address to Route Handlers. DocShell therefore permits requests only when its shared startup guard has attested that the exact server process was bound to the configured port on literal 127.0.0.1 or ::1; missing or inconsistent host/port attestation is rejected. Exact same-origin validation remains an additional browser-side CSRF check, not evidence of the network source.
Next.js · React · TypeScript · Server-Sent Events · IndexedDB · the Claude Code CLI.
MIT
