@tsconfigdotjson/vibe-os 0.2.0-rc.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +19 -847
  2. package/THIRD_PARTY_NOTICES.md +28 -0
  3. package/dist/web/assets/index-82MhMOB4.css +1 -0
  4. package/dist/web/assets/index-82MhMOB4.css.br +0 -0
  5. package/dist/web/assets/index-82MhMOB4.css.gz +0 -0
  6. package/dist/web/assets/index-DCB2nO0u.js +36 -0
  7. package/dist/web/assets/index-DCB2nO0u.js.br +0 -0
  8. package/dist/web/assets/index-DCB2nO0u.js.gz +0 -0
  9. package/dist/web/index.html +2 -2
  10. package/package.json +2 -1
  11. package/scripts/notices.ts +3 -1
  12. package/server/api.ts +449 -20
  13. package/server/assets.generated.ts +13 -13
  14. package/server/attach.ts +65 -16
  15. package/server/browser.ts +1 -65
  16. package/server/cli.ts +48 -150
  17. package/server/client.ts +440 -0
  18. package/server/config.ts +64 -2
  19. package/server/db.ts +19 -0
  20. package/server/doctor.ts +176 -123
  21. package/server/harness.ts +378 -590
  22. package/server/harnesses/claude.ts +248 -0
  23. package/server/harnesses/codex.ts +241 -0
  24. package/server/harnesses/cursor.ts +202 -0
  25. package/server/harnesses/hermes.ts +259 -0
  26. package/server/harnesses/util.ts +181 -0
  27. package/server/headless.ts +358 -0
  28. package/server/index.ts +47 -14
  29. package/server/memory.ts +418 -0
  30. package/server/profiles.ts +27 -15
  31. package/server/projects.ts +52 -3
  32. package/server/reaper.ts +5 -3
  33. package/server/screens.ts +372 -0
  34. package/server/session.ts +84 -95
  35. package/server/setup.ts +73 -7
  36. package/server/windows.ts +51 -1
  37. package/shared/blanks.ts +113 -0
  38. package/shared/harness.ts +375 -0
  39. package/shared/wire.ts +105 -94
  40. package/dist/web/assets/index-B77Qk8B5.js +0 -35
  41. package/dist/web/assets/index-B77Qk8B5.js.br +0 -0
  42. package/dist/web/assets/index-B77Qk8B5.js.gz +0 -0
  43. package/dist/web/assets/index-CH5Em-Mo.css +0 -1
  44. package/dist/web/assets/index-CH5Em-Mo.css.br +0 -0
  45. package/dist/web/assets/index-CH5Em-Mo.css.gz +0 -0
package/README.md CHANGED
@@ -12,6 +12,10 @@ and open terminals into it, from anything with a browser.
12
12
  [![CI](https://github.com/tsconfigdotjson/vibe-os/actions/workflows/ci.yml/badge.svg)](https://github.com/tsconfigdotjson/vibe-os/actions/workflows/ci.yml)
13
13
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](#license)
14
14
 
15
+ <br>
16
+
17
+ <img src="docs/demo.gif" alt="The vibe-os desktop: a Backend Engineer, a QA Engineer and a dev server running in one workspace, then a Reviewer opened from the profiles rail and snapped into the grid">
18
+
15
19
  </div>
16
20
 
17
21
  ---
@@ -34,31 +38,7 @@ edit, no `sshd_config` change, and no root.
34
38
 
35
39
  - [Quick start](#quick-start)
36
40
  - [How it works](#how-it-works)
37
- - [Using it](#using-it)
38
- - [Projects and workspaces](#projects-and-workspaces)
39
- - [Profiles](#profiles)
40
- - [Hermes](#hermes)
41
- - [Hermes and the browser](#hermes-and-the-browser)
42
- - [Cursor](#cursor)
43
- - [Popping a terminal out](#popping-a-terminal-out)
44
- - [The desktop](#the-desktop)
45
- - [Installing it as an app](#installing-it-as-an-app)
46
- - [Deploying on a VPS](#deploying-on-a-vps)
47
- - [What the box needs](#what-the-box-needs)
48
- - [Install](#install)
49
- - [Setup](#setup)
50
- - [Behind Tailscale](#behind-tailscale)
51
- - [Firewall](#firewall)
52
- - [Running it as a service](#running-it-as-a-service)
53
- - [An always-on browser](#an-always-on-browser)
54
- - [Giving the box a GitHub identity](#giving-the-box-a-github-identity)
55
- - [Updating a running box](#updating-a-running-box)
56
- - [Security](#security)
57
- - [CLI reference](#cli-reference)
58
- - [Development](#development)
59
- - [Building from source](#building-from-source)
60
- - [Releasing](#releasing)
61
- - [Project status](#project-status)
41
+ - [Documentation](#documentation)
62
42
  - [Contributing](#contributing)
63
43
  - [License](#license)
64
44
 
@@ -89,7 +69,7 @@ vibe-os setup
89
69
  ```
90
70
 
91
71
  `setup` runs the same checks as `doctor` and offers to fix each one, printing
92
- every command before it runs it. See [Setup](#setup) for what it does.
72
+ every command before it runs it. See [Setup](docs/deploying.md#setup) for what it does.
93
73
 
94
74
  For an agent with SSH access to the box, the prompt is one line:
95
75
 
@@ -131,833 +111,25 @@ reloading the page and closing the laptop all leave the session running.
131
111
 
132
112
  The bottom route in that diagram is the same session by a different door: your
133
113
  own ssh client, your own key, straight to sshd. See
134
- [popping out](#popping-a-terminal-out).
135
-
136
- ---
137
-
138
- ## Using it
139
-
140
- ### Projects and workspaces
141
-
142
- The picker in the top-left switches between git repositories on the machine.
143
- **Refresh** walks the disk for them, bounded by depth and a visit budget, and
144
- only on that button.
145
-
146
- Each project has **workspaces** in the sidebar. A workspace is a git worktree on
147
- its own branch, named with three random words. Creating one fetches `origin` and
148
- branches from its default branch, so it starts level with `origin/main` instead
149
- of with whatever the project's own checkout was left on. That fetch is what the
150
- spinner is waiting for. With no `origin`, or none that can be reached, the
151
- workspace is still created from the last fetch or from the checkout, and the
152
- sidebar says which. Terminals belong to a workspace:
153
- switching swaps which windows are on screen and restores their placement, while
154
- the sessions you left keep running. Only ✕ ends anything.
155
-
156
- `git push` works with no arguments from a new workspace, because creating one
157
- sets `push.autoSetupRemote` on the project. Nothing is pushed for you.
158
-
159
- Every window in a workspace shares one checkout, one branch and one git index.
160
- That is the point, but it means two agents running `git commit` at the same
161
- moment will collide on `index.lock`. Nothing is corrupted, one of them retries.
162
- If two pieces of work need to proceed independently, give them a workspace each.
163
-
164
- ### Profiles
165
-
166
- A profile is a name, a colour, a harness (`claude`, `hermes`, `cursor`, a
167
- shell, or any command on the box) with its flags, and a standing prompt. Profiles belong to a
168
- project and appear in every workspace of it.
169
-
170
- The colour is the point: it tints the window, its title bar and its dock entry,
171
- so three roles running at once are distinguishable without reading anything.
172
-
173
- - **Quitting the harness closes the window.** A role window exists to run that
174
- role. ⟳ opens it again.
175
- - **Clicking a role that is already open raises it.** Hover a live row and press
176
- **+** to open a second one.
177
- - **Flags are stored as a list of arguments**, quoted individually when the
178
- command is built. The browser never sends a command at all.
179
-
180
- The editor gives you a model dropdown, a thinking dropdown (`--effort`), a
181
- permission dropdown and a few switches, with the generated flags and a preview
182
- behind **Advanced**. Models and permission modes are read off the Claude binary
183
- installed on the server, so the list follows Claude's releases.
184
-
185
- #### Hermes
186
-
187
- Picking the Hermes harness runs `hermes chat` in the worktree, and the editor
188
- offers a model, a provider, a thinking dropdown (`--reasoning`), an interface
189
- (`--cli` or `--tui`) and the same kind of switches, `--yolo` among them.
190
-
191
- Models are typed rather than picked. `hermes model` is an interactive wizard
192
- with no listing mode, so there is nothing to read off the box. The provider list
193
- is real: it is whatever `~/.hermes/config.yaml` has been set up with, so adding
194
- one with `hermes model` makes it appear here. Reasoning levels are read off
195
- `hermes chat --help`, so that ladder follows Hermes' releases.
196
-
197
- #### Hermes and the browser
198
-
199
- Hermes' Browser Use mode replaces its twelve browser tools with one that drives
200
- a browser over CDP. Pointed at the always-on browser below, that is the Chrome
201
- you can watch over VNC.
202
-
203
- ```bash
204
- vibe-os connect-hermes
205
- ```
206
-
207
- That sets `browser.cdp_url`, `browser.backend` and `browser.cloud_provider` in
208
- `~/.hermes/config.yaml`. `install-browser` does it as its last step; run it by
209
- hand when Hermes arrives after the browser did. It is a box-wide setting, not a
210
- per-profile one: Hermes reads the CDP target from its config file and has no
211
- flag for it.
212
-
213
- The agent shares that browser with whatever is signed in to it, including the
214
- Claude extension. That is the point (it inherits your sessions) and it is the
215
- cost (it acts inside your accounts).
216
-
217
- Browser Use mode needs the `browser-use` CLI, or `uvx` to run it on demand.
218
- Without either, Hermes keeps its twelve built-in browser tools and says nothing.
219
- `vibe-os doctor` is where that shows up.
220
-
221
- #### Cursor
222
-
223
- Picking the Cursor harness runs `cursor-agent` in the worktree, and the editor
224
- offers a model and a handful of switches. Two of them matter most: `--force`,
225
- which is Cursor's spelling of skip-every-check, and `--trust`, which matters
226
- here more than anywhere else: every workspace is a freshly cut worktree, which
227
- to Cursor is a directory it has never seen, so without it every new window opens
228
- on the trust prompt instead of the conversation.
229
-
230
- Models are typed with real suggestions behind them. `cursor-agent models` is a
231
- proper listing mode — the only harness with one — but the list belongs to the
232
- account rather than the binary: it needs a login, and sometimes the network, so
233
- when it cannot be read the field is plain free text and typed ids still work.
234
-
235
- Logging in is a property of the box, not of a profile: run `cursor-agent
236
- login` once on the box. Logged out, a Cursor window opens and sits at the
237
- login prompt — `vibe-os doctor` says so before you find out that way.
238
-
239
- MCP servers are a property of the box as well. Configure them in
240
- `~/.cursor/mcp.json`, then log in once, from inside any window:
241
-
242
- ```sh
243
- cursor-agent mcp login linear
244
- ```
245
-
246
- Cursor files MCP credentials per directory, which would mean one login per
247
- workspace. vibe-os points every workspace at a single store under the state dir
248
- instead, so that one login covers the ones opened later too. A switch,
249
- `--approve-mcps`, skips the approval prompt each new workspace would otherwise
250
- ask for.
251
-
252
- #### MCP servers
253
-
254
- For Claude profiles. Cursor reads its own list from `~/.cursor/mcp.json`, see
255
- [Cursor](#cursor) above.
256
-
257
- Three settings rather than a list of switches, because that is what Claude
258
- accepts: everything the box has, nothing, or exactly what you pick.
259
-
260
- The list is read off the box. Add one and it appears:
261
-
262
- ```sh
263
- claude mcp add --scope user linear --transport sse https://mcp.linear.app/sse
264
- ```
265
-
266
- Use `--scope user`. Without it Claude files the server under whichever directory
267
- you ran the command in, and if that was a workspace, the server goes when the
268
- worktree does.
269
-
270
- Picking servers writes one file per server under `~/.vibe-os/mcp/` at 0600, and
271
- the profile stores the path rather than the definition, which can contain an API
272
- key. The files are rewritten from `~/.claude.json` whenever a window starts, so
273
- `claude mcp add` stays the only place a server is configured.
274
-
275
- Credentials follow the server name, so a server you have already logged into
276
- keeps working. `--strict-mcp-config` also drops plugin-provided servers, but not
277
- claude.ai connectors like Gmail or Drive, which are attached to your account.
278
-
279
- #### Blanks
280
-
281
- A prompt can leave gaps:
282
-
283
- ```
284
- Read {{which files}} on branch {{branch}} and report back.
285
- ```
286
-
287
- Each `{{…}}` becomes a field in the band above the terminal, drawn inline in the
288
- sentence. Select a word in the editor and press **+ blank** to make one.
289
-
290
- Blanks are keyed by position, so two `{{file}}` stay two separate fields.
291
- Pressing Copy or Send with blanks empty refuses once, then goes anyway, and an
292
- unfilled blank falls back to its own label.
293
-
294
- **Copy** puts the prompt on the clipboard. **Send** writes it into the session
295
- with bracketed paste, so a multi-line prompt arrives in Claude's composer as one
296
- block rather than submitting on the first newline. Send always works;
297
- `navigator.clipboard` does not exist on a page served over plain HTTP to an IP.
298
-
299
- ### Popping a terminal out
300
-
301
- The ⇗ button sends a terminal to its own browser window, or to a real terminal
302
- on your machine:
303
-
304
- ```bash
305
- ssh -t vibe@vibe-os.your-tailnet.ts.net vibe-os attach quiet-amber-otter-1
306
- ```
307
-
308
- Every route addresses the same window id and the same dtach session, so popping
309
- out is detaching one client and attaching another. **Bring it back** returns it
310
- with scrollback intact, and the desktop reclaims a window on its own if the
311
- terminal goes away.
312
-
313
- `vibe-os attach` with no argument gives you a picker of every window on the box:
314
-
315
- ```
316
- vibe-os · 3 windows
317
-
318
- 1 live quiet-amber-otter-1 QA Engineer vibe-os/quiet-amber-otter
319
- 2 idle quiet-amber-otter-2 terminal vibe-os/quiet-amber-otter
320
- 3 live brave-copper-lynx-1 Backend Manager vibe-os/brave-copper-lynx
321
-
322
- attach [1-3, q to quit]:
323
- ```
324
-
325
- **This grants no access.** That ssh connection authenticates with your own key,
326
- or your tailnet identity under `tailscale up --ssh`. vibe-os is not in the auth
327
- path and `--token` does not gate it. Anyone who can ssh to the box could already
328
- type `dtach -a`.
329
-
330
- ### The desktop
331
-
332
- Windows snap to a 24 × 14 grid. Drag a title bar to move, any edge to resize,
333
- double-click to fill the desktop. **⊞** on the dock tiles up to four windows.
334
-
335
- There are no keyboard chords. The terminal has focus nearly all the time, and
336
- every key the desktop took would be a key the session could not have.
337
-
338
- The dock and the profile rail hide themselves. Push the pointer into the bottom
339
- or right edge to bring them back.
340
-
341
- Closing a window ends its session. **Minimise** puts it away and keeps it
342
- running.
343
-
344
- The dock's **◑** opens the wallpaper picker. Uploads are stored on the server,
345
- so the same desktop appears on every device. **Dim** darkens the wallpaper
346
- behind the windows.
347
-
348
- ### Installing it as an app
349
-
350
- vibe-os ships a web app manifest, so Chrome, Edge and Brave offer to install it
351
- and it runs in its own window. On iOS, **Share → Add to Home Screen**.
352
-
353
- It needs a secure origin: `localhost`, or HTTPS by way of
354
- [`tailscale serve`](#behind-tailscale) or `--domain`.
355
-
356
- Give each box its own colour so you can tell installed instances apart:
357
-
358
- ```bash
359
- vibe-os start --theme-color '#7a4fd6'
360
- ```
361
-
362
- The service worker caches nothing. It exists only because Chrome will not offer
363
- installation without one.
364
-
365
- ---
366
-
367
- ## Deploying on a VPS
368
-
369
- ### What the box needs
370
-
371
- | Package | What uses it | Without it |
372
- | --- | --- | --- |
373
- | `openssh-server` | every window logs in through it | nothing connects |
374
- | `openssh-client` | signing certificates, finding the host key | **the server refuses to start** |
375
- | `dtach` | keeps windows alive across reloads | windows die on reload, and **profiles launch no harness** |
376
- | `git` | projects and worktrees | no projects |
377
- | `gh` | PRs, issues and reviews (optional) | git works, the GitHub API does not |
378
- | `claude` | the Claude harness | those profiles open a window that closes immediately |
379
- | `hermes` | the Hermes harness (optional) | those profiles open a window that closes immediately |
380
- | `cursor-agent` | the Cursor harness (optional) | those profiles open a window that closes immediately |
381
- | `uv` | `uvx browser-use`, for Hermes' Browser Use mode (optional) | Hermes keeps its twelve built-in browser tools |
382
-
383
- ```bash
384
- sudo apt update && sudo apt install -y openssh-server openssh-client dtach git gh
385
- curl -fsSL https://claude.ai/install.sh | bash # standalone, needs no Node
386
-
387
- # Optional, for the Hermes harness
388
- curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
389
- curl -LsSf https://astral.sh/uv/install.sh | sh
390
- hermes setup # pick a provider
391
-
392
- # Optional, for the Cursor harness
393
- curl https://cursor.com/install -fsS | bash
394
- cursor-agent login # one login per box
395
- ```
396
-
397
- Verified on Debian 12 and 13, Ubuntu 24.04 and 26.04 LTS, x86\_64.
398
-
399
- `dtach` is the one people skip. The harness command lives in the dtach
400
- invocation, so without it a profile opens a shell and does nothing else.
401
-
402
- ### Install
403
-
404
- ```bash
405
- curl -fsSL https://raw.githubusercontent.com/tsconfigdotjson/vibe-os/main/install.sh | sh
406
- ```
407
-
408
- It installs the latest [release](https://github.com/tsconfigdotjson/vibe-os/releases)
409
- for Linux x64, Linux arm64 or Apple Silicon to `/usr/local/bin` (or
410
- `~/.local/bin` without sudo), after checking its SHA-256. Set `VIBE_OS_VERSION`
411
- to pin a tag, or `VIBE_OS_INSTALL_DIR` to choose the directory.
412
-
413
- The binary carries the whole app, including the 20MB SSH WASM runtime. No Bun,
414
- Node or npm on the target.
415
-
416
- On a box that already has Bun, the npm package is the same thing without the
417
- build step:
418
-
419
- ```bash
420
- bun add -g @tsconfigdotjson/vibe-os # installs the vibe-os command
421
- ```
422
-
423
- Then check it, twice. `vibe-os doctor` answers whether a certificate this host
424
- signs will actually be accepted, which catches the failures that otherwise
425
- appear in a browser as `handshake failed` and nowhere else. Reading sshd's
426
- effective config and the firewall needs root:
427
-
428
- ```bash
429
- vibe-os doctor # everything that does not need privileges
430
- sudo vibe-os doctor # adds the sshd and firewall checks
431
- ```
432
-
433
- `doctor` evaluates the flags it is given and cannot see a systemd unit, so pass
434
- the same flags the service uses to get a true answer.
435
-
436
- ### Setup
437
-
438
- ```bash
439
- vibe-os setup
440
- ```
441
-
442
- Each step checks first and skips itself when there is nothing to do, so running
443
- it again on a configured box changes nothing. In order:
444
-
445
- 1. **User.** As root, it offers to create a user with passwordless sudo and
446
- root's SSH keys, then stops so you can log in as them and run it again.
447
- 2. **Prerequisites.** `openssh-server`, `openssh-client`, `dtach`, `git`,
448
- `curl` and optionally `gh`, through apt, dnf, yum, pacman or zypper.
449
- 3. **sshd**, enabled and started if nothing answers on port 22.
450
- 4. **Harnesses.** Claude Code, then optionally Hermes and Cursor.
451
- 5. **Tailscale**, installed and brought up (it prints a login URL).
452
- 6. **The service**, on `127.0.0.1:7681` with a generated token kept in
453
- `~/.vibe-os/config.json`. An installed unit is left alone unless you pass a
454
- different `--port` or `--host`.
455
- 7. **`tailscale serve --bg 7681`**, for HTTPS on the tailnet.
456
- 8. **SSH hardening.** Writes `01-hardening.conf` to turn off password logins,
457
- runs `sshd -t` and reloads. Skipped if your user has no SSH key yet.
458
- 9. **Firewall**, with ufw: allow the tailnet and `41641/udp`, deny the rest.
459
- 10. **`doctor`**, run with sudo, then the URL with `?token=`.
460
-
461
- The firewall step can lock you out, so before enabling ufw it arms a systemd
462
- timer that disables it again in five minutes. Setup then waits for you to
463
- confirm from a **new** session over the tailnet:
464
-
465
- ```bash
466
- ssh <user>@<machine>.<tailnet>.ts.net vibe-os confirm-firewall
467
- ```
468
-
469
- If that never arrives, ufw turns itself off. Your provider's firewall is
470
- separate; leave `41641/udp` open there.
471
-
472
- | flag | |
473
- | --- | --- |
474
- | `-y`, `--yes` | take the default answer to every question |
475
- | `--firewall` | with `--yes`, do the firewall step too (it is skipped otherwise) |
476
- | `--hermes`, `--cursor` | install those harnesses too |
477
- | `--user <name>` | as root, the user to create |
478
- | `--port`, `--host` | where the service listens (default `7681`, `127.0.0.1`) |
479
-
480
- With `--yes`, an agent runs the firewall confirmation from a second SSH
481
- connection while setup waits in the first.
482
-
483
- ### Behind Tailscale
484
-
485
- The recommended setup. You stop needing to bind port 80, and you stop needing
486
- Let's Encrypt.
487
-
488
- ```bash
489
- curl -fsSL https://tailscale.com/install.sh | sh
490
- sudo tailscale up
491
-
492
- # Loopback, not the tailnet address: serve proxies to 127.0.0.1, and binding
493
- # the tailnet address gives a 502 because serve cannot reach it.
494
- vibe-os start --port 7681 --host 127.0.0.1 --token
495
- sudo tailscale serve --bg 7681
496
- ```
497
-
498
- That serves it at `https://<machine>.<tailnet>.ts.net` with a real certificate,
499
- which is also what the browser needs before it will expose the clipboard or
500
- offer to install the app.
501
-
502
- ### Firewall
503
-
504
- Tailscale does not close ports for you. Until you do, the machine still answers
505
- on its public address, and vibe-os hands out shells.
506
-
507
- **Confirm you can reach the box over the tailnet in a second terminal before
508
- denying anything**, and know where your provider's rescue console is.
509
-
510
- ```bash
511
- sudo ufw allow in on tailscale0
512
- sudo ufw allow 41641/udp # keeps Tailscale on a direct path
513
- sudo ufw default deny incoming
514
- sudo ufw default allow outgoing
515
- sudo ufw enable
516
- ```
517
-
518
- Verified from outside the tailnet against a box running this recipe: ports 22,
519
- 80, 443, 7681 and 8080 all time out, while the tailnet address answers.
520
-
521
- `sudo vibe-os doctor` checks these. It reads ufw, falling back to nftables and
522
- iptables, and it reads configuration rather than reachability. It cannot see
523
- your provider's firewall, which is a separate thing worth closing too. OVH's is
524
- stateless, so leave `41641/udp` open there or Tailscale relays everything.
525
-
526
- Cloud images often ship `PasswordAuthentication yes` in
527
- `/etc/ssh/sshd_config.d/50-cloud-init.conf`. sshd takes the first occurrence of
528
- a keyword, so overriding it needs a file that sorts *earlier*:
529
-
530
- ```bash
531
- printf 'PasswordAuthentication no\nKbdInteractiveAuthentication no\n' \
532
- | sudo tee /etc/ssh/sshd_config.d/01-hardening.conf
533
- sudo sshd -t && sudo systemctl reload ssh
534
- ```
535
-
536
- Confirm a fresh key-based login works before closing the session you have.
537
-
538
- ### Running it as a service
539
-
540
- ```bash
541
- sudo vibe-os install-service --port 7681 --token hunter2
542
- journalctl -u vibe-os -f
543
- ```
544
-
545
- Run it with `sudo` from your own account, not as root. It takes the user from
546
- `SUDO_USER` and writes a unit that runs as them, forwarding whatever flags you
547
- passed. It refuses to write a unit that runs as root.
548
-
549
- If you are binding port 80 rather than using Tailscale, that needs
550
- `CAP_NET_BIND_SERVICE`, which `install-service` grants. By hand:
551
-
552
- ```bash
553
- sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/vibe-os
554
- ```
555
-
556
- Otherwise vibe-os falls back to port 8080 and says so.
557
-
558
- ### An always-on browser
559
-
560
- One Google Chrome, on a virtual display, running for as long as the box does.
561
- It is where a browser extension lives on a machine with no screen. Optional, and
562
- separate from `install-service`.
563
-
564
- x86\_64 only. Google publishes no arm64 Chrome for Linux, and Chromium is not a
565
- substitute if you want the Claude in Chrome extension.
566
-
567
- ```bash
568
- sudo apt install -y tigervnc-standalone-server openbox x11-utils
569
-
570
- curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
571
- | sudo gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
572
- echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] \
573
- https://dl.google.com/linux/chrome/deb/ stable main" \
574
- | sudo tee /etc/apt/sources.list.d/google-chrome.list
575
- sudo apt update && sudo apt install -y google-chrome-stable
576
-
577
- sudo vibe-os install-browser
578
- ```
579
-
580
- That writes seven units: an X server with VNC built in, a window manager, Chrome
581
- itself, a liveness probe on a timer, and a nightly restart on a timer. All of
582
- them restart on failure.
583
-
584
- | Flag | Default |
585
- | --- | --- |
586
- | `--geometry <WxH>` | `1600x900` |
587
- | `--display <n>` | `99` |
588
- | `--vnc-port <n>` | `5900`, loopback only |
589
- | `--cdp-port <n>` | `9222`, loopback only |
590
- | `--restart-at <expr>` | `*-*-* 02:00:00`, in the box's timezone |
591
- | `--no-restart` | install no nightly timer |
592
- | `--vnc-password [value]` | none, and an existing one is kept |
593
- | `--no-vnc-password` | serve the display with no authentication |
594
-
595
- The default names the box's own timezone, which on most VPS images is UTC, and
596
- the install summary prints it. Pass your own zone if your night is elsewhere, as
597
- in `*-*-* 02:00:00 America/New_York`.
598
-
599
- Give it two gigabytes of headroom counting swap. Chrome idles near 0.5GB, and on
600
- a box without it the OOM killer takes terminal sessions rather than tabs.
601
-
602
- ```bash
603
- sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
604
- sudo mkswap /swapfile && sudo swapon /swapfile
605
- echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
606
- ```
607
-
608
- #### Using it
609
-
610
- The display has no VNC password. It listens on loopback, so reach it through
611
- SSH, and `sudo vibe-os doctor` fails loudly if it is ever bound anywhere else.
612
-
613
- ```bash
614
- ssh -L 5900:127.0.0.1:5900 you@host
615
- ```
616
-
617
- Point any VNC viewer at `127.0.0.1:5900`. TigerVNC Viewer, RealVNC Viewer and
618
- Remmina all connect straight through.
619
-
620
- macOS Screen Sharing will not take a server with no authentication: it asks for
621
- a password that does not exist. Give it one, keeping the loopback bind as the
622
- layer that actually protects the box:
623
-
624
- ```bash
625
- sudo vibe-os install-browser --vnc-password # generates and prints one
626
- sudo vibe-os install-browser --vnc-password hunter2
627
- ```
628
-
629
- Then open `vnc://127.0.0.1:5900` and leave the username blank. VNC
630
- authentication is password-only.
631
-
632
- Later runs of `install-browser` keep a password that is already set, so a
633
- reinstall does not quietly drop you back to no authentication. `--no-vnc-password`
634
- turns it off on purpose.
635
-
636
- VNC authentication is DES-based and truncates at 8 characters, so treat it as a
637
- second layer and not as the thing keeping the box shut. Prefer the bare
638
- `--vnc-password`: a password passed as an argument is visible in shell history
639
- and, briefly, in `ps`.
640
-
641
- ```bash
642
- systemctl restart vibe-os-chrome # bounce it now
643
- journalctl -u vibe-os-chrome -f # what it is saying
644
- sudo systemctl stop vibe-os-chrome vibe-os-wm vibe-os-xvnc # stop all of it
645
- ```
646
-
647
- Open a URL in the running instance from a terminal on the box:
648
-
649
- ```bash
650
- DISPLAY=:99 google-chrome https://example.com
651
- ```
652
-
653
- The nightly restart closes leftover tabs and returns the memory a day of
654
- browsing took. The profile lives in `~/.vibe-os/chrome` and carries sign-ins
655
- across a restart, so this does not log you out.
656
-
657
- #### Extensions
658
-
659
- Install by hand through the viewer, or force-install without any interaction:
660
-
661
- ```bash
662
- sudo mkdir -p /etc/opt/chrome/policies/managed
663
- sudo tee /etc/opt/chrome/policies/managed/vibe-os.json <<'JSON'
664
- {
665
- "ExtensionInstallForcelist": [
666
- "fcoeoabgfenejglbffodgkkbkcdhcgfn;https://clients2.google.com/service/update2/crx"
667
- ]
668
- }
669
- JSON
670
- sudo systemctl restart vibe-os-chrome
671
- ```
672
-
673
- That ID is Claude in Chrome. Take any other from its Chrome Web Store URL.
674
-
675
- Signing in is interactive, including a captcha, so do it once through the
676
- viewer. Claude in Chrome needs a paid plan.
677
-
678
- Anyone who reaches this browser gets whatever it is signed in to. That is the
679
- same token and the same tailnet that already hand out shells, but it is a wider
680
- blast radius than a shell alone.
681
-
682
- ### Giving the box a GitHub identity
683
-
684
- Workspaces are branches, and pushing happens in the terminal, so a box with no
685
- GitHub credentials dead-ends at the first `git push`. Do this as the login user,
686
- not root:
687
-
688
- ```bash
689
- ssh-keygen -t ed25519 -C "vibe-os@$(hostname)" -f ~/.ssh/id_ed25519 -N ""
690
- cat ~/.ssh/id_ed25519.pub
691
- ```
692
-
693
- No passphrase: an agent-less prompt would appear inside whichever window ran
694
- `git push`, where nothing can answer it.
695
-
696
- Add that key to [github.com/settings/keys](https://github.com/settings/keys)
697
- twice, as an **Authentication key** and as a **Signing key** if you want commits
698
- made here to show as verified:
699
-
700
- ```bash
701
- git config --global gpg.format ssh
702
- git config --global user.signingkey ~/.ssh/id_ed25519.pub
703
- git config --global commit.gpgsign true
704
- git config --global user.name "Your Name"
705
- git config --global user.email "you@example.com" # a verified GitHub email
706
- ```
707
-
708
- The key covers git. It does not cover the GitHub API, so PRs and issues need
709
- `gh auth login` (choose SSH, which keeps the token confined to API calls).
710
-
711
- A push-capable key here means anyone who gets a shell can push to your
712
- repositories. Use a per-repository deploy key instead if that matters, and
713
- accept that `gh` will not work.
714
-
715
- ### Updating a running box
716
-
717
- ```bash
718
- curl -fsSL https://raw.githubusercontent.com/tsconfigdotjson/vibe-os/main/install.sh | sh
719
- sudo systemctl restart vibe-os
720
- ```
721
-
722
- Nothing is lost by that restart. Two things it does not pick up:
723
-
724
- - **A changed harness command.** An existing session keeps running whatever it
725
- was started with. Close the window and open it again.
726
- - **Changed service flags.** Those live in the unit, so re-run
727
- `install-service`.
728
-
729
- A reboot is different: dtach sessions do not survive one.
730
-
731
- ---
732
-
733
- ## Security
734
-
735
- **vibe-os hands out shell access to the machine it runs on.** Every window is
736
- the same unix user, there is no per-user isolation, and anyone who can reach the
737
- port and pass the gate can run anything that user can. Treat access to vibe-os
738
- as equivalent to SSH access to the box.
739
-
740
- Report vulnerabilities privately, as described in [SECURITY.md](SECURITY.md).
741
-
742
- The recommended shape is all of it: behind Tailscale, firewalled to the tailnet,
743
- bound to the tailnet address, and with the token on.
744
-
745
- **The gate is on by default.** The first start generates a token, remembers it
746
- in the state dir, and prints the URL with it.
747
-
748
- ```bash
749
- vibe-os # the remembered token, or a new one
750
- vibe-os --token hunter2 # or pick your own
751
- vibe-os --no-token # no gate, with a warning at startup
752
- ```
753
-
754
- It gates the app, the API, the certificate signer, the wallpaper upload and the
755
- WebSocket bridge.
756
-
757
- Two checks run in front of it:
758
-
759
- - **Origin, always.** A request carrying an `Origin` that is not this server is
760
- refused, so a page on another site cannot drive the API or open the bridge
761
- through your browser.
762
- - **Host, with no token.** A request has to name the server by an address,
763
- `localhost`, the machine's hostname, `--domain`, `--ssh-advertise` or its
764
- Tailscale name. That stops DNS rebinding, where a page points its own domain
765
- at the box to become same-origin with it. Add any other name with
766
- `--allowed-host`. With a token this check is off, since a rebound page has
767
- neither the cookie nor the token.
768
-
769
- Open the URL with `?token=…` once and the server sets an `HttpOnly` cookie, then
770
- redirects without the token so it does not linger in history. Scripts can send
771
- `Authorization: Bearer <token>`. Comparison is constant-time.
772
-
773
- The cookie value is the token rather than a derived session id, so there is no
774
- per-browser session to revoke. Rotating means changing it on the server.
775
-
776
- `--domain` provisions a Let's Encrypt certificate over HTTP-01 and serves HTTPS,
777
- renewing 30 days before expiry. On plain HTTP the browser clipboard API is
778
- unavailable, so copy-on-select and paste stop working and the app cannot be
779
- installed. OSC 52 clipboard *reads* are always refused, since anything running
780
- in a session could otherwise ask what you last copied.
114
+ [popping out](docs/using.md#popping-a-terminal-out).
781
115
 
782
116
  ---
783
117
 
784
- ## CLI reference
785
-
786
- | command | what it does |
787
- | --- | --- |
788
- | `vibe-os` / `vibe-os start` | serve the UI and the SSH bridge |
789
- | `vibe-os attach [window]` | attach this terminal to a window's session |
790
- | `vibe-os doctor` | check this machine is ready, and say what is missing |
791
- | `vibe-os setup` | fix what `doctor` finds, asking before each change |
792
- | `vibe-os confirm-firewall` | tell a waiting `setup` the tailnet still gets in |
793
- | `sudo vibe-os install-service` | write and enable a systemd unit |
794
- | `sudo vibe-os install-browser` | run one Chrome on a virtual display |
795
- | `vibe-os connect-hermes` | point Hermes' browser tools at that Chrome |
796
- | `vibe-os fetch-wasm` | re-download the SSH WASM runtime |
797
-
798
- ```
799
- --port <n> HTTP port (default 80)
800
- --host <addr> bind address (default 0.0.0.0)
801
- --domain <fqdn> provision a Let's Encrypt certificate and serve HTTPS
802
- --email <addr> contact address for Let's Encrypt
803
- --tls-port <n> HTTPS port (default 443)
804
-
805
- --token [value] the token to require (default: the last one used, or
806
- a new one, remembered in the state dir)
807
- --no-token disable the gate (prints a warning)
808
- --allowed-host <name>
809
- with no token, a name this server answers to beyond
810
- its hostname, --domain and its Tailscale name.
811
- Repeatable
812
-
813
- --ssh-host <addr> SSH target for the bridge (default 127.0.0.1)
814
- --ssh-port <n> SSH target port (default 22)
815
- --ssh-advertise <host[:port]>
816
- host to print in attach commands, when sshd is not on
817
- the name the browser reached the desktop on
818
- --user <name> unix user to log in as (default: current user)
819
- --no-sessions plain login shells instead of persistent dtach sessions
820
- --cert-ttl <secs> certificate lifetime (default 43200)
821
-
822
- --theme-color <hex> window chrome colour for the installed app (default #1c2128)
823
- --workspace <dir> where worktrees are created (default ~/workspace)
824
- --state-dir <dir> CA, TLS material and generated MCP configs
825
- (default ~/.vibe-os)
826
- ```
827
-
828
- `install-browser` takes its own flags:
829
-
830
- ```
831
- --geometry <WxH> virtual screen size (default 1600x900)
832
- --display <n> X display number (default 99)
833
- --vnc-port <n> VNC port, bound to loopback (default 5900)
834
- --cdp-port <n> Chrome debug port, bound to loopback (default 9222)
835
- --restart-at <expr> nightly restart, a systemd OnCalendar expression
836
- (default '*-*-* 02:00:00', the box's own timezone)
837
- --no-restart do not install the nightly restart timer
838
- --vnc-password [value]
839
- require a VNC password; generates and prints one if
840
- omitted. macOS Screen Sharing will not connect without
841
- this. An existing one is kept unless --no-vnc-password
842
- --no-vnc-password serve the display with no authentication
843
- ```
844
-
845
- `setup` takes the [flags above](#setup) as well as `--port`, `--host`,
846
- `--workspace` and `--state-dir`, which it passes to the service.
847
-
848
- `connect-hermes` takes one:
849
-
850
- ```
851
- --cdp-port <n> the debug port to point Hermes at, when there is no
852
- installed browser unit to read it from
853
- ```
854
-
855
- Every option except `--tls-port` and `--cert-ttl` also reads a `VIBE_OS_`
856
- environment variable, which is how the container is configured. `--no-token` is
857
- `VIBE_OS_NO_TOKEN=1`, and `--allowed-host` is `VIBE_OS_ALLOWED_HOSTS`, comma
858
- separated.
859
-
860
- ---
861
-
862
- ## Development
863
-
864
- ```bash
865
- bun install # also fetches ssh.wasm from the upstream release
866
- bun run build # web app + precompressed assets + embedded manifest
867
- bun run dev # Vite on :5173, proxying to a vibe-os on :7681
868
- bun run dev:server # the server with --hot, on :7681
869
- bun run compile # standalone binaries into dist/bin
870
-
871
- bun run check # lint + typecheck + tests, the same three CI runs
872
- bun test # specs in test/ and beside the code
873
- ```
874
-
875
- Biome with the recommended rule set and Prettier's defaults. Nothing is switched
876
- off in `biome.json`. The server is TypeScript run directly by Bun, with no build
877
- step of its own.
118
+ ## Documentation
878
119
 
879
120
  | | |
880
121
  | --- | --- |
881
- | server | Bun, TypeScript run directly, `Bun.serve`, no framework |
882
- | storage | SQLite (`bun:sqlite`) via drizzle-orm, idempotent DDL at startup |
883
- | browser | React 19, Vite, xterm.js v6, SWR for polling |
884
- | ssh | `ssh.wasm` in the tab, `ssh-keygen` and `ssh-keyscan` on the host |
885
- | shared | `shared/`: wire types, the grid, the argv tokeniser |
886
-
887
- `ssh.wasm` comes prebuilt from
888
- [c2FmZQ/sshterm](https://github.com/c2FmZQ/sshterm) releases, pinned by tag with
889
- its SHA-256 verified before extraction. To move it, set `SSHTERM_VERSION`, run
890
- `bun run fetch-wasm`, and copy the checksum it prints into `PINNED`.
891
-
892
- ### Building from source
893
-
894
- ```bash
895
- bun install && bun run build
896
- bun scripts/compile.ts linux-x64 # or linux-arm64, darwin-arm64; none for all
897
- scp dist/bin/vibe-os-linux-x64 you@host:/tmp/vibe-os
898
- ssh you@host 'sudo install -m 0755 /tmp/vibe-os /usr/local/bin/vibe-os'
899
- ```
900
-
901
- ### Releasing
902
-
903
- Bump `version` in `package.json`, merge, then tag the merge commit:
904
-
905
- ```bash
906
- git tag v0.2.0 && git push origin v0.2.0
907
- ```
908
-
909
- The tag must match `package.json`. The workflow publishes the binaries with
910
- `SHA256SUMS` as a GitHub Release, pushes `ghcr.io/tsconfigdotjson/vibe-os`, and
911
- publishes `@tsconfigdotjson/vibe-os` to npm with provenance, through npm's
912
- [trusted publishing](https://docs.npmjs.com/trusted-publishers). A hyphenated
913
- tag such as `v0.2.0-rc.1` is a prerelease: the installer skips it, and npm gets
914
- it under the `next` dist-tag rather than `latest`.
915
-
916
- ### Icons
917
-
918
- App icons are generated from `public/icon.svg`:
919
-
920
- ```bash
921
- rsvg-convert -w 192 -h 192 public/icon-app.svg -o public/icon-192.png
922
- rsvg-convert -w 512 -h 512 public/icon-app.svg -o public/icon-512.png
923
- rsvg-convert -w 180 -h 180 public/icon-app.svg -o public/apple-touch-icon.png
924
- rsvg-convert -w 512 -h 512 public/icon-maskable.svg -o public/icon-maskable-512.png
925
- ```
926
-
927
- ---
928
-
929
- ## Project status
930
-
931
- Usable and in daily use, but young.
932
-
933
- Everything in [Deploying on a VPS](#deploying-on-a-vps) has been walked end to
934
- end on a fresh OVHcloud VPS on Ubuntu 26.04, behind Tailscale, serving HTTPS.
935
- The firewall rules were checked from outside the tailnet.
936
-
937
- Known gaps:
938
-
939
- - **One token, one user.** Everyone with the token is the same unix user, and
940
- there is no per-browser session to revoke.
941
- - **ACME has never issued a real certificate.** `--domain` works in tests, but
942
- the Tailscale path makes it unnecessary, so it stays unproven.
943
- - **IPv6 reachability is unverified.** The v4 firewall rules were tested from
944
- outside; the v6 rules mirror them and default to `DROP`, but that was read
945
- from the box rather than probed.
946
- - **No keyboard shortcuts.** Deliberate, see [the desktop](#the-desktop), but
947
- something that does not steal keys from the terminal is worth having.
948
- - **MCP servers can be picked but not added.** Adding them is `claude mcp` on
949
- the box.
950
- - **Hermes models are typed, not listed.** `hermes model` is interactive and
951
- has no listing mode, so there is nothing to read off the box.
952
- - **Hermes' browser is a box-wide setting.** Every Hermes profile drives the
953
- same Chrome, because that is where Hermes reads the CDP target from.
954
- - **Cursor's login is a box-wide setting.** The CLI holds one credential per
955
- user, so every Cursor profile runs as the same account, and its model list is
956
- readable only while logged in. Its MCP credentials are shared across every
957
- workspace for the same reason.
958
- - **No branch operations.** Pushing, PRs and merging happen in the terminal.
959
- - **Deleting a workspace keeps its branch**, so work is recoverable. Nothing
960
- prunes them for you.
122
+ | [Using it](docs/using.md) | projects, workspaces, profiles, prompts, popping a terminal out, the desktop and its shortcuts |
123
+ | [Harnesses](docs/harnesses.md) | Hermes, Cursor and Codex, and adding your own |
124
+ | [Deploying on a VPS](docs/deploying.md) | what the box needs, setup, Tailscale, the firewall, memory, an always-on browser, updating |
125
+ | [Security](docs/security.md) | the token, what it gates, and what to put in front of it |
126
+ | [CLI reference](docs/cli.md) | every command and flag |
127
+ | [API](docs/api.md) | driving workspaces and windows over HTTP |
128
+ | [Development](docs/development.md) | building, testing and releasing |
129
+
130
+ **vibe-os hands out shell access to the machine it runs on.** Treat access to it
131
+ as equivalent to SSH access to the box, and read [Security](docs/security.md)
132
+ before exposing it anywhere.
961
133
 
962
134
  ---
963
135