Claude draws its own interface, live, on your machine.
Ask for a panel. Claude writes one as an ordinary HTML file. The page patches it in place the moment the file lands, so scroll position, form input and open sections survive every update. Click any part of it, say what should change, and it comes back changed without a reload.
The terminal stays the engine room. The browser is where you read, look, and point at things. Nothing is sandboxed and nothing is uploaded, because the page and the session are the same machine.
- Panels that change under you. Ask for a chart, a map of the repo, three design directions. Claude writes the file, the page morphs it in place, and what you were looking at stays where it was. Panels are tabs, and they outlive the turn that made them.
- Point at anything and say what's wrong. Click an element, select the words you mean, attach a note. Panels and the window's own furniture are both fair game, so "make this heading smaller" works on a chart and on the top bar alike. Notes accumulate and go over as one pass.
- A window you can rewrite. The top bar, transcript, composer and tabs are files in your workspace. Ask Claude to change them, or open them yourself. CSS swaps live.
- The everyday things too. A transcript with expandable tool calls and a
subagent's whole run nested inside the call that launched it, the
/diffyou wanted with a comment on any line, and plans you reorder and strike before approving.
Type this in Claude Code:
/plugin marketplace add Goodeye-Labs/finestra
/plugin install finestra@goodeye-labs
/reload-plugins
Then /finestra. There is nothing to clone, nothing to symlink, and no settings
file to edit. Stop and the approval gate arrive already wired, because the
hook ships with the plugin.
Requires Node 20+, Claude Code 2.1.220 or newer, and curl. macOS and
Linux; Windows means WSL. Nothing to build and no dependencies, so there is no
npm install step.
The context and cost meter is the one thing a plugin cannot configure for you. It is optional, and Hooks says how to add it.
To update: /plugin update finestra@goodeye-labs. To have it update itself, run
/plugin, open Marketplaces, select goodeye-labs and choose Enable
auto-update.
finestra doctor reports what is wired and what is not, and says which of it is
optional. To remove everything: /plugin uninstall finestra@goodeye-labs.
Run /reload-plugins, or start a new session, since a session already running
has not seen the plugin yet. Then type:
/finestra
You get a URL and a browser window. The left column is already your conversation. Now ask for something that wants a picture:
show me a map of this repo as a panel: top-level directories, what each one
holds, and the biggest file in each
A tab appears. Then hit Review in the top bar, click any heading in that panel, type "make this smaller", and send. The panel changes in place, and you never touched the terminal.
That round trip is the whole product. Everything below is detail.
This is what a working window looks like. Your conversation on the left, with tool calls you can expand, and whatever Claude drew on the right.
If asking for something visual doesn't open a window, type /finestra directly.
Explicit invocation always works.
The Changes tab is the CLI's /diff. Hover a line to comment on it, and the
batch goes back carrying the file, line, hunk and code, which is what makes it
something Claude can act on rather than a note about a page. A commented line
stays marked.
Underneath it is the checkpoint list, rebuilt from what Claude Code records
before each prompt. It is read-only, since restoring is /rewind in the terminal.
Hit Review, click anything, attach a note. Panels and the window's own furniture are both fair game, so "make this heading smaller" works on a chart and on the top bar alike. Pins accumulate and go over as one batch when you send, so a review reads as one pass instead of interrupting on every click. Select words first and the note is about the phrase rather than the whole element.
/ opens a menu of everything you can invoke, read from your skills and
installed plugins, so a skill added on disk shows up without finestra knowing
about it. Built-in commands are listed but marked terminal-only, because nothing
outside the CLI can dispatch /compact.
@ completes paths. ↑ walks your real prompt history for this directory, ^R
searches it. Pasted and dropped images land in the session's assets and go into
the message as a path Claude can read. ⌘K is a command palette over all of it.
⌘F finds in the conversation.
Type while a turn is running and the message is held rather than sent, so it stays editable until the turn ends.
Nothing leaves your machine. No network calls, no telemetry, no dependencies, no
MCP server. The port binds to 127.0.0.1, and curl is used once by the
launcher against that port to find the server it just started.
It reads your session and subagent transcripts, your prompt history, which
sessions are live, and your skills and installed plugins to fill the / menu. In
your project it runs reporting-only git commands for the Changes tab.
It writes panels, layout, assets and the inbox into a scratch workspace keyed
to this session, never your repo. Prompts typed in the window are appended to
~/.claude/history.jsonl, the same file the terminal uses, so ↑ works in both.
That is the one file finestra writes that Claude Code also writes, so the append
is bounded and set FINESTRA_NO_HISTORY=1 to turn it off.
A human sends every message. finestra never prompts your session on its own, and it holds no credential: it reads files Claude Code already wrote and writes files Claude Code reads. Every model call is made by your own Claude Code.
Panel HTML is injected without sanitizing, deliberately: Claude authoring
arbitrary HTML is the entire point, and it already has full filesystem access in
your session. So don't point FINESTRA_STATE at a directory you didn't generate,
and don't expose the port. Chat text is a different matter and is escaped before
a small markdown subset is applied.
The window dies with its session, and the launcher clears workspaces whose session has ended. To close one sooner, end the session or kill the server.
Closed the tab by accident? Run /finestra again. It reopens the same window
rather than starting a second one.
To uninstall: /plugin uninstall finestra@goodeye-labs. That takes the hook with
it. Drop the statusLine entry too if you added one.
The interrupt hook needs nothing from you. It ships in the plugin's own
hooks/hooks.json, so installing wires it and uninstalling removes it.
interrupt.sh is what makes Stop land mid-turn: a message from the window queues
behind the running turn by design, and a PreToolUse hook is the only thing that
runs while a turn is in flight. It is global, so it runs before every tool call in
every project, and when finestra is idle it is two file tests and an exit.
The context meter needs one line. A plugin's own settings may only carry a
couple of keys, and statusLine is not among them, so this is the one thing left
to add by hand. Ask Claude for it, or merge this into ~/.claude/settings.json:
{
"statusLine": {
"type": "command",
"command": "sh -c 'exec node \"$(ls -d \"$HOME\"/.claude/plugins/cache/*/finestra/*/ | sort -V | tail -1)hooks/statusline.mjs\"'",
"refreshInterval": 5
}
}That resolves the newest installed copy every time it runs. Naming one version's path directly also works, and then quietly stops working at the next update, because each version is installed beside the last rather than over it.
Merge it into what is already there rather than replacing the file. Claude Code picks it up without a restart. Skip it and everything else still works: the window says plainly that the meter is missing rather than showing a blank gauge.
statusline.mjs feeds context usage, cost and rate-limit headroom, none of which
is in the transcript. It still prints a status line, because it is one.
With the PreToolUse hook installed, the palette offers to hold shell commands,
or every tool call, until you say so. This is finestra's own checkpoint rather
than a view of Claude Code's permission prompt, which is drawn by the terminal
and never reaches disk.
The gate only ever tightens. It either denies a call or says nothing, and it cannot approve past a rule that would have stopped one. In a session running with prompts turned off, it reinstates a human checkpoint that would otherwise not be there.
Holding a turn is the kind of feature that wedges a session when it goes wrong, so it is off unless you arm it, it comes down when the window closes, waiting always ends, and every failure path allows. A broken gate behaves exactly like an absent one.
One window, one session, bound when the server starts and never moved. There is no switcher: reaching a session means something inside it is watching its inbox, and nothing outside can make a cold session start watching, so a list of sessions you cannot talk to is worse than no list.
Two sessions in one directory do not collide. Separate workspaces, inboxes, shells, assets and ports, all keyed to the session id.
The workspace defaults to scratch: Claude Code's own per-project temp root when
that exists, otherwise ${TMPDIR:-/tmp}/finestra/<slug>. A window belongs to one
session and dies with it, so leaving it in a working tree would litter every repo
with state nothing outlives the conversation to use.
Set FINESTRA_STATE to keep panels somewhere durable instead. A directory you
name is yours, and is never swept.
FINESTRA_PORT (default 4747, stepping up to the next free port unless you set
it), FINESTRA_PROJECT (default cwd), FINESTRA_SESSION (the session to show),
FINESTRA_STATE (where the window's files live), and FINESTRA_NO_OPEN (start
without opening a browser).
The launcher is the normal path, because it knows the session id and ties the server's life to it. To run one yourself, name the session:
FINESTRA_STATE=/tmp/finestra FINESTRA_SESSION=<id> node src/server.mjsGive it FINESTRA_STATE, or it falls back to .finestra/ inside the project.
A server started this way has no owner, so it keeps running after the session
ends and you have to stop it yourself.
On first run the window copies its own interface into the workspace and serves it
from there. The top bar, transcript, composer and tabs are all files you can
change, by asking Claude or by opening them. CSS swaps live. A file you have
edited is never overwritten by an upgrade, and POST /shell/reset restores the
shipped version.
Panels inherit the theme through :host([data-theme='dark']) rather than a media
query, because the toggle in the top bar has to beat the OS setting.
scripts/demo.mjs serves a window on a synthetic session, and
scripts/shot.mjs photographs one headlessly in its own browser profile:
node scripts/demo.mjs # prints a URL
node scripts/shot.mjs http://localhost:4788 out.png # from another shellnpx playwright install chromium # once, for the UI suite's real browser
npm testThree suites run in order and the first failure stops the rest: the markdown
subset, then the server against a synthetic transcript, then the real page driven
over CDP. None of them touch ~/.claude or any live session.
npm run test:core stops after the first two, which need no browser. The UI suite
and shot.mjs need Node 22+ for a global WebSocket, while the product
itself runs on 20, which is what CI checks.
Zero dependencies, no MCP server, and nothing in the critical path that depends on a research-preview feature.
terminal (claude) browser (127.0.0.1:4747)
│ │
│ writes transcripts │ SSE ▲ │ POST ▼
▼ │ │
~/.claude/projects/…/<session>.jsonl ──────┤ │
~/.claude/projects/…/subagents/*.jsonl ─────┤ │
server.mjs │
<state>/sessions/<id>/panels/*.html ─watch──┤ │
<state>/sessions/<id>/shell/* ─watch──┘ │
<state>/sessions/<id>/inbox.jsonl ◄─────────────────┘
│
└── tail -f, via Claude Code's Monitor tool ──► back into the session
Claude writes a panel with the ordinary Write and Edit tools, fs.watch picks it
up, and the page morphs it into the DOM with Idiomorph rather than replacing it.
The server tails the transcript JSONL Claude Code already writes, which carries messages, tool calls and results, permission mode and which skill drove each message, so none of the display needs Claude's cooperation. Subagents are not in that file and write their own beside it, which are tailed too.
Coming back the other way, typed messages, pins and answers are appended to the
session's inbox.jsonl, and Claude watches its own inbox with a persistent
Monitor. Questions work the same way: AskUserQuestion blocks on the terminal's
prompt, so Claude writes the question to a file and ends its turn instead, and the
answer returns through the inbox with nothing blocking.
The reason all of it is files: the server holds no durable state, so killing it or reloading the tab costs nothing, and you can hand-edit a panel because the watcher doesn't care who wrote the file.
v0.1, one session per window. Text arrives in paragraphs rather than word by
word, because token streaming only works with --print, which would mean giving
up the interactive terminal.
/compact, /clear, /model and /rewind are dispatched inside the TUI, and
nothing outside the process can trigger a built-in command. Claude can invoke a
skill, because that is a tool call. An interrupt lands at the next tool call, so a
pure-text reply cannot be caught.
Claude Code documents where it stores a transcript but treats the contents as internal and free to change between releases, so on a version finestra has not seen you get a quietly reduced window rather than an error.
Claude Code is the only harness supported today. Everything that knows its file
layout lives in src/harness/, so a second one is an adapter rather than a
rewrite.
No hosted mode, no multi-user, no cloud relay, no account, no MCP server. It handles no credential and makes no network request. A human sends every message.
These are design decisions rather than a roadmap. The window is worth having because it runs entirely on one machine, and each of these would cost that.
MIT. See LICENSE.
finestra is not affiliated with, endorsed by, or sponsored by Anthropic. It reads files that Claude Code writes on your own machine, and Claude Code itself remains Anthropic's software under Anthropic's terms.


