instabox 0.7.0 → 0.7.1

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.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # instabox (npm)
2
2
 
3
3
  ```
4
- npx instabox@latest # sign in, install the CLI and the agent skills
4
+ npx instabox@latest # sign in, install the CLI and the agent skills (--no-skills: no skills)
5
5
  npx instabox@latest sh # the same, then start a new box and SSH into it
6
6
  ```
7
7
 
8
8
  `npx instabox@latest` signs you in to instabox with a device code (first run only: open the page it prints on any device and enter the code; it opens the page for you where there is a browser, never with `--no-browser`), installs the `instabox` CLI already signed in or updates an out-of-date one (skip with `--no-cli`), installs or updates the agent skills from [`../skills`](../skills) in `~/.claude/skills` and `~/.agents/skills` (skip with `--no-skills`), and prints the next steps. It does not touch your boxes.
9
9
 
10
- `npx instabox@latest sh` does all that, then starts a new box (`box-<hex>`) and drops you into it over SSH, no questions. Run by an agent (no terminal), it starts the box and prints `instabox ssh <box> -- <command>` instead of opening a shell. At the box limit it starts nothing and lists your boxes, to remove one (`instabox rm <box>`) or open one (`instabox ssh <box>`).
10
+ `npx instabox@latest sh` does all that, then starts a new box (`box-<hex>`) and drops you into it over SSH, no questions. Run by an agent (no terminal), it starts the box and prints `instabox ssh <box> -- <command>` instead of opening a shell. At the box limit it starts nothing and lists your boxes, to remove one (`instabox rm --yes <box>`) or open one (`instabox ssh <box>`).
11
11
 
12
12
  A GitHub and a Google sign-in are separate instabox accounts, each with its own boxes. Both this entry point and `instabox login` let the sign-in page offer both. `--google` asks for Google directly, as does `instabox login --provider google`.
13
13
 
@@ -19,7 +19,7 @@ Any other command line is the instabox CLI's ([`cmd/instabox`](../cmd/instabox))
19
19
  - `sh` always starts a new box, with no timeout and no idle auto-suspend of its own. An existing box is entered with the CLI: `instabox ssh <box>`.
20
20
  - An installed CLI is checked against what the server's `/install.sh` would install: the binary's sha256 against the server's `/dl/SHA256SUMS` (release builds are named after a commit, so their versions do not sort). One the server no longer serves is replaced in place through the same installer; a development build (`instabox dev`, or `-dirty`) is left alone with a one-line notice of the install command.
21
21
  - Skills are written only over copies marked `installed-by: instabox`; the output says which were installed, updated, already up to date, or kept because they are the user's own.
22
- - On Windows, where `/install.sh` does not run, the CLI is installed by `lib/cli.mjs` itself: `/dl/instabox-windows-<arch>.exe`, checked against `SHA256SUMS` and `--version`, renamed to `%LOCALAPPDATA%\Programs\instabox\instabox.exe`. The output gives the PowerShell line that puts that folder on the user's PATH; `npx instabox@latest <command>` finds it without that.
22
+ - On Windows, where `/install.sh` does not run, the CLI is installed by `lib/cli.mjs` itself: `/dl/instabox-windows-<arch>.exe`, checked against the `X-Instabox-Sha256` header sent with it (`SHA256SUMS` on older servers; a mismatch downloads it again, up to 3 times) and `--version`, renamed to `%LOCALAPPDATA%\Programs\instabox\instabox.exe`. The output gives the PowerShell line that puts that folder on the user's PATH; `npx instabox@latest <command>` finds it without that.
23
23
  - The bin is also called `instabox`, so `lib/cli.mjs` finds the real CLI the way the insta CLI does: PATH without `node_modules` dirs and without links back to this script, then the installer's own targets.
24
24
  - `prepack` copies `../skills` into the package; every skill there ships on the next publish.
25
25
 
package/bin/instabox.mjs CHANGED
@@ -57,7 +57,7 @@ function usage() {
57
57
  ${bold('instabox')}: a cloud box in seconds
58
58
 
59
59
  npx instabox@latest sign in with a device code (first time), install the instabox
60
- CLI and agent skills
60
+ CLI and agent skills (~/.claude/skills, ~/.agents/skills)
61
61
  npx instabox@latest sh the same, then start a new box and SSH into it
62
62
  --github, --google sign in with that provider
63
63
  --no-browser only print the sign-in page and code; do not open a browser
@@ -171,7 +171,9 @@ async function quotaFull(err) {
171
171
  const w = Math.max(...boxes.map((b) => b.name.length));
172
172
  for (const b of boxes) console.log(` ${b.name.padEnd(w)} ${b.state}`);
173
173
  }
174
- console.log(`\n Remove one with ${bold('instabox rm <box>')}, or open one with ${bold('instabox ssh <box>')}.\n`);
174
+ console.log(
175
+ `\n Remove one with ${bold('instabox rm --yes <box>')}, or open one with ${bold('instabox ssh <box>')}.\n`,
176
+ );
175
177
  process.exit(1);
176
178
  }
177
179
 
package/lib/cli.mjs CHANGED
@@ -176,16 +176,35 @@ async function installWindows(
176
176
  const get = async (file) => {
177
177
  const res = await fetch(`${api}/dl/${file}`, { signal: AbortSignal.timeout(300_000) });
178
178
  if (!res.ok) throw new Error(`download ${file}: HTTP ${res.status}`);
179
- return Buffer.from(await res.arrayBuffer());
179
+ return { body: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get('x-instabox-sha256') };
180
180
  };
181
- const [body, sums] = await Promise.all([get(name), get('SHA256SUMS')]);
182
- const want = String(sums)
183
- .split('\n')
184
- .map((l) => l.trim().split(/\s+/))
185
- .find(([, n]) => n === name || n === `*${name}`)?.[0];
186
- if (!/^[0-9a-f]{64}$/.test(want || '')) return { error: `no valid checksum for ${name} in SHA256SUMS` };
187
- const got = createHash('sha256').update(body).digest('hex');
188
- if (got !== want) return { error: `checksum mismatch for ${name}; run it again if a deploy was in progress` };
181
+ // The sha256 from the same response: behind a load balancer, SHA256SUMS
182
+ // can come from a host serving another build. Older servers lack it.
183
+ let body, sums;
184
+ for (let attempt = 1; ; attempt++) {
185
+ const res = await get(name);
186
+ let want = res.sha256;
187
+ let from = 'the X-Instabox-Sha256 header';
188
+ if (!want) {
189
+ sums ??= String((await get('SHA256SUMS')).body);
190
+ want = sums
191
+ .split('\n')
192
+ .map((l) => l.trim().split(/\s+/))
193
+ .find(([, n]) => n === name || n === `*${name}`)?.[0];
194
+ from = `${api}/dl/SHA256SUMS`;
195
+ }
196
+ if (!/^[0-9a-f]{64}$/.test(want || '')) return { error: `no valid checksum for ${name} in ${from}` };
197
+ const got = createHash('sha256').update(res.body).digest('hex');
198
+ if (got === want) {
199
+ body = res.body;
200
+ break;
201
+ }
202
+ if (attempt === 3)
203
+ return {
204
+ error: `checksum mismatch for ${name} after ${attempt} downloads of ${api}/dl/${name} (got ${got}, want ${want} from ${from}); run it again if a deploy was in progress`,
205
+ };
206
+ await new Promise((r) => setTimeout(r, 1000));
207
+ }
189
208
  mkdirSync(to, { recursive: true });
190
209
  writeFileSync(tmp, body, { mode: 0o755 });
191
210
  const v = await version(tmp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "A cloud box for your coding agent, the instabox CLI and its agent skills in one command: npx instabox@latest",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,18 +18,20 @@ instabox login # device code: open the printed page, enter the cod
18
18
 
19
19
  A GitHub sign-in and a Google sign-in with the same verified email share one account; with different emails they are separate accounts with separate boxes. Both `instabox login` and `npx instabox@latest` sign in with a device code: they print a page and a code (and open the page when there is a browser); the user opens it on any device, enters the code and chooses GitHub or Google there. It works the same over SSH or on a machine without a browser. Relay the page and the code to the user and wait; the command finishes on its own. For Google directly, use `instabox login --provider google`, or `npx instabox@latest --google`. The CLI shares the sign-in `npx` made.
20
20
 
21
+ `instabox update` replaces the CLI with the server's current build (it prints "already up to date" when it is); run it when a command the docs describe is missing. `instabox completion bash|zsh|fish|powershell` prints a shell completion script for the user's shell startup file.
22
+
21
23
  For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it in the dashboard: API keys in the sidebar). Never print, commit or log a key.
22
24
 
23
25
  ## Templates
24
26
 
25
- `agent-browser` is the default, so `create` needs no `--template` for ordinary work. It is Debian with git, gh, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker, Claude Code and Codex, plus a desktop (VNC, Chromium) ready for browsing or driving a GUI from the start. The dashboard shows it as **standard**. Pick another template only for its one purpose:
27
+ `agent-browser` is the default, so `create` needs no `--template` for ordinary work. It is Debian 13 (trixie, amd64, kernel 6.18.54) with git, gh, Node.js 22, Python 3.13 (`python3 -m venv`, pip, uv), Go, Rust, a C toolchain, Docker, Claude Code and Codex, plus a desktop (VNC, Chromium) ready for browsing or driving a GUI from the start, on a 16 GiB root disk and a 20 GiB persistent disk at `/data`. The dashboard shows it as **standard**. Pick another template only for its one purpose:
26
28
 
27
29
  - `universal`: the same tools without the desktop, which installs itself the first time it is used (about half a minute); a smaller root disk. The dashboard shows it as **basic**.
28
- - `herdr`: for the herdr client.
29
- - `orca`: for Orca desktop and mobile.
30
- - `t3`: for the T3 Code desktop and mobile clients.
30
+ - `herdr`: for the herdr client. The dev tools (gh, Node.js 22, Python 3.13 with venv and pip, Claude Code, Codex, the desktop) without Docker, uv, Go, Rust or a C toolchain; an 8 GiB root disk.
31
+ - `orca`: for Orca desktop and mobile. The same tools as `herdr` plus the Orca runtime instead of herdr; an 8 GiB root disk.
32
+ - `t3`: for the T3 Code desktop and mobile clients. `universal` plus the T3 Code server (port 3774); a 16 GiB root disk.
31
33
 
32
- `instabox templates` lists the ones this server offers. The free plan creates boxes from `agent-browser` only: another template is refused with `403 template_not_allowed` and needs a paid plan (the user upgrades on the dashboard). A box from a snapshot (`--snapshot`) works on every plan, whatever template the snapshot came from. Do not retry another template on the free plan; use `agent-browser`.
34
+ `instabox templates` lists the ones this server offers (`--json` also lists retired ones with `"creatable": false`, which no new box can start from). The free plan creates boxes from `agent-browser` only: another template is refused with `403 template_not_allowed` and needs a paid plan (the user upgrades on the dashboard). A box from a snapshot (`--snapshot`) works on every plan, whatever template the snapshot came from. Do not retry another template on the free plan; use `agent-browser`. Over the HTTP API, `POST /v1/machines` with an unknown `templateId` is `400 bad_request` whose message lists the templates your plan can create from; a body field the API does not have is `400 unknown_field` naming the closest real one (`template`: did you mean `templateId`?); `name` may be left out (`box-` and 8 random hex digits).
33
35
 
34
36
  ## Sizes
35
37
 
@@ -48,36 +50,53 @@ A box is 4 vCPU / 8 GB of memory unless `create` names another size of the accou
48
50
  ```sh
49
51
  instabox templates # base systems to start from
50
52
  instabox create --name work # new box; waits until it is running
53
+ instabox create --name work --setup-file ./setup.sh # then runs the script in it
51
54
  instabox ls # your boxes and their states
52
55
  instabox ssh work # interactive shell (leave with: exit)
53
56
  instabox exec work -- uname -a # one command, no SSH client needed
54
57
  instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
58
+ instabox run -- make -v # one-shot: new box, run, delete it, exit with the code
59
+ instabox ps work # its processes: pid, ppid, user, %cpu, %mem, elapsed, command
55
60
  instabox files work /data/home # list a directory (or print a text file)
61
+ instabox push work ./app # copy a file or directory in (to /data/home/app)
62
+ instabox pull work /data/home/app/out.log # copy a file or directory out (to ./out.log)
63
+ instabox cp ./x.tgz work:/data/home/x.tgz # scp-style, either way (cp work:/data/home/x.tgz .)
56
64
  instabox auto-suspend work 10m # idle time before a paid box pauses
57
65
  instabox max-run work 120 # pause a paid box after two running hours
58
66
  instabox max-run work -1 # remove the runtime cap; idle policy still applies
59
67
  instabox stop work && instabox start work # stop ends processes, keeps disks
60
68
  instabox suspend work && instabox resume work # pause/resume keeps running processes
69
+ instabox restart work # power-cycle a hung box (stop, start); disks kept
61
70
  instabox rename work api # rename in place, any state; ID and port URLs stay
62
- instabox rm work # delete the box
71
+ instabox rm --yes work # delete the box (without --yes it asks y/N)
63
72
  ```
64
73
 
65
74
  - Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
66
- - `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--max-run-minutes N`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--no-wait`.
75
+ - `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--max-run-minutes N`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--no-wait`, `--setup-file PATH|-`, `--fail-fast` (when no host has room, fail at once and create nothing instead of failing the create after; with `--snapshot`, instead of creating it stopped).
76
+ - `create --setup-file PATH` (`-`: stdin; at most 64 KB) runs that script in the new box once it is running, in HOME as the box's user (by its `#!` line, else with bash; CRLF line endings become LF), and prints its output; it replaces create + push + exec for provisioning. A failing script makes `create` exit with the script's code and keeps the box: debug it with `instabox exec work -- /tmp/instabox-setup.sh`. With `--json` the sandbox object gets a `setup` object (`exitCode`, `stdout`, `stderr`, `signal`, `timedOut`, `truncated`, `durationMs`). The script is killed after `--setup-timeout` (default and at most 10m); for longer installs, use `ssh`. A missing, empty or over-64 KB file is refused before any box is created. `run --setup-file` does the same before its command, with its `-e` variables; when the script fails, run's `--json` `exitCode` is the script's. The script stays at `/tmp/instabox-setup.sh` (and in snapshots), so keep credentials out of it.
67
77
  - Idle auto-pause defaults to 30 minutes. Paid boxes allow 5, 10, 15, 20, 25 or 30 minutes, or `0` to turn it off. Free boxes keep 30 minutes unless an admin grants an idle override. Idle means no agent working, no terminal use and little CPU.
68
78
  - Max run minutes is independent of idle activity: `-1` is unlimited, `0` pauses immediately, and a positive integer forces a pause after that many running minutes. Free boxes have a fixed 480-minute cap that only an admin session can change; paid boxes default to `-1` and can change it. An idle override does not permit changes to a free box's runtime cap. The first limit reached pauses the box; both timers restart after sleep and wake.
69
79
  - A runtime pause blocks every wake path during a server-configured cooldown, 10 minutes by default. Wait until the `cooldownUntil` deadline shown by `instabox get`; edits cannot clear it. Afterward, an automatically paused box wakes on `exec`, `ssh`, `files`, a public port request or `instabox wake <box>`. A zero runtime cap must be changed before waking. A normal pause preserves memory and processes.
80
+ - If `exec`/`ssh` stop answering (hang or time out on a running box), run `instabox restart <box>` (alias `recover`; `--timeout 20s` shortens the wait, `--json` prints the box). It stops the box, killing the VM if it has not shut down by the timeout (60 seconds by default), then starts it and waits until it runs. Files on the root disk and `/data` stay; processes do not, so start your servers again. A paused box is not hung: resume it. A box stuck in `starting` with no start in progress cannot be restarted; restart says so.
81
+ - Disk full (`df -h / /data`)? Grow it with `instabox resize <box> --disk N` (`/data`, where HOME is) and/or `--root-disk N` (`/`), in GB. Disks only grow, and only while the box is stopped: on a running or paused box add `--restart` (it stops the box, grows the disks and starts it again; files stay, processes do not), else resize changes nothing and says so. Free-plan boxes keep their size.
70
82
  - Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
83
+ - Use `cp`, `push` and `pull` to move files, not `echo <base64> | base64 -d` over `exec`. `cp LOCAL work:PATH` and `cp work:PATH LOCAL` copy either way (`-r` for directories). `push work LOCAL [REMOTE]` copies a file or directory to REMOTE (default: HOME); an existing directory or a path ending in `/` receives it under its own name. REMOTE's parent directory must exist (else `missing_parent`); `-p` creates it. A file streams byte-for-byte, up to 5 GiB, and its sha256 is checked; a directory goes as a gzipped tar of at most 1 GiB. From inside a git repo, a directory push sends only tracked files (`git ls-files -c`); `--all` adds untracked files git does not ignore; `node_modules`, build output and `.git` stay behind. `pull work REMOTE [LOCAL]` copies back the same way: a file streams, any size; a directory is read as root, at most 32 MiB compressed. Over HTTP: `PUT /v1/machines/{id}/files?path=P` with the raw body, `GET /v1/machines/{id}/files/content?path=P`, and `POST /v1/machines/{id}/files/upload-url` for a signed URL that takes the PUT without a key.
71
84
  - `exec` with one argument after `--` runs it as a shell string (`instabox exec work -- 'ls | wc -l'`); several arguments run as that exact argv, each quoted for you (`instabox exec work -- bash -c 'echo a b > /tmp/x'`).
72
- - `exec` returns the command's exit code and takes `--stdin` to pipe input. It stops the command after `--timeout` seconds: 60 by default, 600 at most. Run longer commands (installs, builds, test suites) over SSH, which also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`. The box's runtime cap still applies during SSH or exec.
73
- - The box's HOME has `MACHINE.md` (which box it is, its size, what survives a stop, the installed tools) and `AGENTS.md`. Both are worth reading before working in the box; they appear a moment after it starts.
85
+ - For a one-off job that needs no box afterwards, use `instabox run [--template T | --from SNAPSHOT] [--name N] [--push DIR] [--cwd D] [-e K=V ...] [--timeout 60s] [--keep] [--setup-file PATH|-] [--json] -- <cmd...>` instead of create + exec + rm: it deletes the box even when the command fails or the run is interrupted (Ctrl-C, SIGTERM), and exits with the command's code (124: timed out). Its own failures have their own codes: 2 usage, 75 `--fail-fast` and no host has room (retry later), 125 `run` itself failed (API, network, create, upload or cleanup), 130 interrupted; with `--json` they print the `{"error": {...}}` object. One argument is a shell string, several an exact argv. The same 10-minute limit as `exec` applies. `--push .` first uploads the current directory's files (those git does not ignore) into the working directory (`--cwd`, default HOME), so `instabox run --push . -- 'go test ./...'` tests your checkout. `--name N` is used as is (1-63 lowercase letters, digits or `-`, starting with a letter or digit; default `run-<8 hex digits>`). A box left behind by a killed CLI is deleted by the server at its `deleteAt`, 5 minutes after its timeout (10 with `--push`, plus another timeout with `--setup-file`); a cleanup delete that gets a 5xx, a retryable error or no answer is retried 5 times with backoff. `--setup-file PATH|-` runs a setup script (at most 64 KB) first, in HOME with the same `--timeout` and `-e` variables (after `--push`); if it fails, `run` exits with its code without running the command. `--keep` keeps the box with no deadline and, once it was created, prints `Kept NAME; delete it with: instabox rm --yes NAME` to stderr (stdout stays the command's; `--json` has it as `sandbox`); a failed create operation prints only its error (one whose wait was cut short is kept, with `Kept NAME, whose create had not finished; …`).
86
+ - `instabox ps work [--all] [--json]` lists the processes running in the box (it runs `ps` through exec, so it wakes a box that paused itself and refuses a stopped or manually paused one). Kernel threads and the listing's own processes (its shell, `ps`, `head` and the sshd sessions exec uses) are left out unless `--all`. `--json` prints an array of `{pid, ppid, user, cpuPercent, memPercent, elapsedSeconds, command}`: `instabox ps work --json | jq '.[] | select(.command | test("node"))'`. It lists only what runs now; there is no history of past commands or their output.
87
+ - To copy a box (try something risky, or run agents side by side from the same state), use `instabox fork work [--name N] [--count N] [--keep-snapshot] [--json]` (`clone` works too): it saves a snapshot of `work` (live while it runs, where the server allows), creates `--count` boxes (1-5) from it with the same template and size, waits until they run, and deletes the snapshot, also on error or Ctrl-C, unless `--keep-snapshot`. `work` is not changed. Several boxes are named `N-1` to `N-N`. It is snapshot + create, so it takes about as long as those two; each box counts toward the plan's starts a minute, which fork waits out (up to 5 minutes in total). If the source's size is no longer one of the plan's sizes, pass `--vcpus N --mem-mib M`. On a failure part-way it prints which boxes were created; they are kept, so delete those you do not need. `--json` prints `{source, machines, snapshotId, snapshotName, kept, snapshotMs, createMs, totalMs}`.
88
+ - `exec` returns the command's exit code and takes `--stdin` to pipe input. `exec --json` prints one object instead of the raw output (`stdout`, `stderr`, `exitCode`, `signal`, `timedOut`, `truncated`, `durationMs`) and still exits with the command's code: `instabox exec work --json -- make test | jq .exitCode`. It stops the command after `--timeout` seconds: 60 by default, 600 at most. Run longer commands (installs, builds, test suites) over SSH, which also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`. The box's runtime cap still applies during SSH or exec.
89
+ - For work past 600 s, or to keep going while you do something else, start a background job: `instabox exec --background work -- 'npm run build'` prints a job ID at once (`--json`: `{jobId, pid}`); then `instabox jobs ls work`, `instabox jobs logs work <job> -f` (streams until it ends, exits with its code; Ctrl-C leaves the job running), `instabox jobs wait work <job> [--timeout 30m]` and `instabox jobs kill work <job> [--signal KILL]` (the whole process group). This is the CLI polling over plain exec calls, not server-side streaming. A job holds no activity lease: only its CPU use, or a running `jobs wait`/`logs -f`, counts as activity, so keep `jobs wait` going for a quiet job you need to finish. A pause freezes the job and it carries on after resume; a stop ends jobs (`exited(143)`, or `lost` after a hard stop).
90
+ - Output: `--json` (or `-o json`) works on every command (each command's `-h` lists it); `ssh`, `mosh` and `port-forward` are interactive and refuse it with the JSON error code `json_unsupported` (use `exec --json`). With JSON output a failure prints `{"error":{"code","message",...}}` on stdout instead of text on stderr and still exits non-zero (2 for usage errors, code `usage`); API errors keep the server's `code`, `status` and `requestId`. Piped, the list and status commands (`ls`, `get`, `templates`, `usage`, `snapshot ls`, `port ls`, `keys ls`, `secrets ls`, `vault ls`, `workspace ls|get|members|invites`) print JSON on their own, so `instabox ls | jq` works; `--output table` or `INSTABOX_NO_AUTO_JSON=1` keeps the table. `exec`, `ssh`, `files` and the other commands print the same output piped or not.
91
+ - The box's HOME has `MACHINE.md` (which box it is, its size, what survives a stop, the installed tools) and `AGENTS.md`, a moment after it starts. They are reference for when a task needs a fact about the box, not steps to run first.
74
92
  - A public URL for a web server in the box: `instabox port publish work 3000` prints it (`instabox port ls`, `instabox port unpublish work 3000`). The URL works while the box runs.
93
+ - A private tunnel instead, only from your computer: `instabox port-forward work 8080:3000` (alias `forward`; `LOCAL` defaults to `REMOTE`, several ports at once) serves the box's port 3000 at `http://127.0.0.1:8080` over SSH and stays in the foreground until Ctrl-C, so start it in the background (`instabox port-forward work 8080:3000 &`) and kill it when done. It fails at once when the local port is busy.
75
94
 
76
95
  ## Organization Secrets
77
96
 
78
97
  Secrets are encrypted API credentials owned by an org, each limited to the HTTPS hosts it may be sent to. A box that selects a Secret gets a placeholder in its environment, never the credential itself. When a request from the box goes to one of the Secret's allowed hosts over HTTPS, the outbound proxy swaps the placeholder in the request headers for the credential. Code in the box can call the API but cannot read or copy the credential. No header-name configuration is needed.
79
98
 
80
- Secrets suit HTTP API keys and tokens that a client sends in a request header: model providers, GitHub's API, deploy and SaaS APIs. They do not cover database URLs or passwords, signing keys, or anything a client hashes, encodes or sends outside an HTTPS header; those need their own setup on the box.
99
+ Secrets suit HTTP API keys and tokens that a client sends in a request header: model providers, GitHub's API, deploy and SaaS APIs. HTTP Basic auth in the `Authorization` header also works, so `git clone` and `git push` over HTTPS work when the Secret's hosts include `github.com` and git gets the placeholder from a credential helper such as `gh auth setup-git`. They do not cover database URLs or passwords, signing keys, or anything a client hashes, signs, encodes some other way or sends outside an HTTPS header; those need their own setup on the box.
81
100
 
82
101
  Secrets replace the Vault, which is legacy and will be retired: save every new API credential as a Secret, never in the Vault, and do not suggest `vault set`, `set-file` or `import` to users. A new box without `--secrets` still gets an existing user's Vault items (see the last point below). Opt into Secrets with `create --secrets`, or use `secrets select` later. Metadata labels do not select Secrets, and Secrets do not deliver files.
83
102
 
@@ -96,11 +115,11 @@ instabox create --name tenant --no-vault --secrets none
96
115
  - `--host` is optional and repeatable. Give each exact hostname the credential may be sent to; without `--host` (or with `--host '*'`) every public HTTPS destination is allowed. Schemes, ports, paths, IP addresses, and partial wildcards such as `*.example.com` are rejected. Use the service's API host, such as `api.openai.com`, never a guess from the credential's value.
97
116
  - Org commands default to the signed-in user's personal org. Use `--org` for a team, and select only Secrets from the box's org. Value, host and deletion changes require an org owner/admin sign-in. API keys with the appropriate machine scopes can inspect metadata and manage sandbox selections, but cannot retrieve or change Secret values.
98
117
  - `secrets select BOX --secrets` replaces the box's selection: `all`, `none`, comma-separated names, or a JSON array (which allows a Secret literally named `all` or `none`). Keep existing names when adding one.
99
- - Placeholders work only in HTTPS request headers on port 443 to the allowed hosts. Do not hash, encode, or sign with a placeholder; other traffic carries the placeholder unchanged.
118
+ - Placeholders work only in HTTPS request headers on port 443 to the allowed hosts. Apart from Basic auth in `Authorization`, which the proxy decodes and re-encodes, do not hash, encode, or sign with a placeholder; other traffic carries the placeholder unchanged.
100
119
  - Rotating a value keeps its placeholder, so running clients need no change. Newly selected names appear in new shells that load the managed environment; restart existing applications to pick them up.
101
120
  - For a box intended for another person or tenant, disable personal Vault injection with `--no-vault` and select only the org Secrets they may use. For a fresh box with neither system's credentials, use `--no-vault --secrets none`. An empty Secret selection alone still permits Vault injection. These flags do not remove credentials already copied into a source snapshot.
102
121
  - For an existing box, disable Vault with `start BOX --no-vault` or `resume BOX --no-vault` and clear the Secret selection with `secrets select BOX --secrets none`. Refresh a running guest with both `vault push BOX` and `secrets push BOX`. Vault values already copied into processes or files remain there; disabling Vault does not recover them.
103
- - The Vault is legacy and will be retired; every `vault` command prints a warning. Its commands remain only for items a user already saved: `vault ls`, `show`, `push` and `rm`, and `vault export` to recover a retained item. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store copies or deletes the other. To move a Vault env item that is an HTTP API credential, the user enters its value again as a Secret with its hosts (`secrets set NAME --host HOST` reads it from a hidden prompt), you select it on the box, then `vault rm` it with the user's go-ahead. Never read a Vault value to copy it. Database credentials and other non-HTTP secrets are not supported by Secrets and need their own setup on the box; Vault file items likewise.
122
+ - The Vault is legacy and will be retired; every `vault` command prints a warning. Its commands remain only for items a user already saved: `vault ls`, `show`, `push` and `rm`, and `vault export` to recover a retained item. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store copies or deletes the other. To move a Vault env item that is an HTTP API credential, the user enters its value again as a Secret with its hosts (`secrets set NAME --host HOST` reads it from a hidden prompt), you select it on the box, then `vault rm --yes` it with the user's go-ahead. Never read a Vault value to copy it. Database credentials and other non-HTTP secrets are not supported by Secrets and need their own setup on the box; Vault file items likewise.
104
123
 
105
124
  ## Moving work onto a box
106
125
 
@@ -126,6 +145,7 @@ instabox feedback --json --type bug --component cli --area exec \
126
145
 
127
146
  ## Habits
128
147
 
129
- - Forgotten boxes pause themselves after 30 idle minutes. Delete a box with `instabox rm` once its work is done.
148
+ - Forgotten boxes pause themselves after 30 idle minutes. Delete a box with `instabox rm --yes NAME` once its work is done. `rm`, `snapshot rm`, `secrets rm`, `vault rm` and `keys rm` ask y/N first and, off a terminal with no answer, delete nothing and fail, so pass `--yes` (`-y`).
130
149
  - When the user asks for a new box, create one, with a name `instabox ls` does not already show. Offer an existing box only when they want to continue earlier work in it.
131
150
  - `instabox usage` shows storage use and limits. A quota error means delete or stop something; do not retry the same create in a loop.
151
+ - `no_capacity` (`create` and `run` exit 75 with `--fail-fast`; otherwise `create` exits 1, `run` 125) is different: no host has room right now and nothing was kept. It is retryable: retry the same command with backoff (about 15 s, 30 s, 60 s, then every few minutes), or try another size (`--vcpus/--mem-mib`) or template.
@@ -9,7 +9,7 @@ metadata:
9
9
 
10
10
  Use the user's chosen org and sandbox IDs. Secrets are shared within an org. A sandbox that selects a Secret gets a placeholder in its environment; the outbound proxy puts the token into HTTPS request headers sent to the Secret's allowed hosts, and nowhere else. Other org sandboxes whose selection is `all`, including future sandboxes, can also use a newly saved Secret. Updating an existing Secret affects every sandbox already selecting it.
11
11
 
12
- Only tokens in the built-in registry (`TOKEN_HOSTS` in `scripts/lib.mjs`) are saved, each for its service's API hosts: for example `ANTHROPIC_API_KEY` for `api.anthropic.com`, `OPENAI_API_KEY` for `api.openai.com`, `GH_TOKEN` and `GITHUB_TOKEN` for `api.github.com`. Every other credential-looking export becomes a manual step. Secrets work only for clients that send the token in an HTTPS header; database URLs, signing keys and git's own credential exchange need their own setup on the box.
12
+ Only tokens in the built-in registry (`TOKEN_HOSTS` in `scripts/lib.mjs`) are saved, each for its service's API hosts: for example `ANTHROPIC_API_KEY` for `api.anthropic.com`, `OPENAI_API_KEY` for `api.openai.com`, `GH_TOKEN` and `GITHUB_TOKEN` for `api.github.com` and `github.com`. Every other credential-looking export becomes a manual step. Secrets work only for clients that send the token in an HTTPS header (Basic auth included); database URLs and signing keys need their own setup on the box. git over HTTPS (`github.com`) also needs git to get the placeholder from a credential helper such as `gh auth setup-git` on the box.
13
13
 
14
14
  Git and editor configuration is copied separately, once, to the named running sandboxes. It is not stored as Secrets, does not consume Secret quota, and is not reapplied on start or resume. This skill moves no repository or agent history. Use `instabox-move-repo` for code and conversation history.
15
15
 
@@ -83,7 +83,7 @@ The pusher verifies org ownership, target orgs, Secret IDs and versions, input l
83
83
 
84
84
  Only selected token names are added to each target's selection; existing names are preserved, and a target selecting `all` stays `all`. Running targets receive an immediate refresh; stopped targets receive the selection at their next start, but config copying requires them to be running. A failed or pending refresh does not install config that may depend on those credentials. Existing processes need to reload their environment or restart to see new placeholders.
85
85
 
86
- Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free. No git credential helper is added: git over HTTPS sends credentials in a form the proxy cannot fill in, so the box needs its own git sign-in (for example `gh auth login` there). An existing org Secret by itself is not a sandbox selection; add it through an explicitly reviewed `instabox secrets select BOX --secrets ...` that keeps the other selected names.
86
+ Files are transformed before direct copying: this machine's HOME becomes `/data/home`, recognized credential lines are removed, macOS-only lines and unsupported signing/SSH rewrites are commented out. Structured config suspected of containing credentials is rejected rather than copied verbatim. This filtering is best effort, not a guarantee that arbitrary configuration is credential-free. No git credential helper is added: the proxy fills the placeholder into git's Basic auth, but git on the box needs a helper that returns the placeholder (for example `gh auth setup-git` there). An existing org Secret by itself is not a sandbox selection; add it through an explicitly reviewed `instabox secrets select BOX --secrets ...` that keeps the other selected names.
87
87
 
88
88
  The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs, versions and hosts, and copied sandbox paths, never values. It does not grant reuse across orgs or across deletion and recreation of a Secret. It records completed work for a repeat plan; it does not sync changes automatically.
89
89
 
@@ -96,6 +96,6 @@ instabox exec <box> -- 'test "${GH_TOKEN+x}" = x && test -f "$HOME/.gitconfig"'
96
96
 
97
97
  Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove the upstream accepts the token. Confirm client behavior without logging credentials, and explain application restarts.
98
98
 
99
- Report the org Secret names and hosts saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving Secrets without targets changes no selection; replacing a value still affects sandboxes already selecting it. Say that git over HTTPS needs its own sign-in on the box, and that credentials outside the registry (database URLs, cloud keys, other tokens) were not moved and need their own setup there; an HTTP API key can be added with `instabox secrets set NAME --org ORG --host HOST`. Future boxes created without `--secrets` keep their existing Vault behavior. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope.
99
+ Report the org Secret names and hosts saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving Secrets without targets changes no selection; replacing a value still affects sandboxes already selecting it. Say that git over HTTPS needs a credential helper such as `gh auth setup-git` on the box, and that credentials outside the registry (database URLs, cloud keys, other tokens) were not moved and need their own setup there; an HTTP API key can be added with `instabox secrets set NAME --org ORG --host HOST`. Future boxes created without `--secrets` keep their existing Vault behavior. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope.
100
100
 
101
101
  Config edits in a sandbox are not overwritten at every resume. Shell aliases, SSH keys, cloud credential files, agent sign-in databases and repositories are outside this inventory. Complete the selected manual sign-in steps only within existing authorization. Remove `$W` when no longer needed; it contains metadata and choices, not credential values.
@@ -41,15 +41,16 @@ export const SECRET_KEY_LINE =
41
41
  // request header. The box gets a placeholder and the outbound proxy fills in the token only for
42
42
  // these hosts. A token whose client signs with it, sends it in a body or URL, or talks to hosts
43
43
  // that cannot be named exactly is left out: the scan lists it as a manual step.
44
- // FORMAT is the scan and approved-selection format. 4: proxy-only Secrets, hosts from
45
- // TOKEN_HOSTS. Older files approved env delivery or other hosts; they are refused, never
46
- // reinterpreted, so an approval cannot widen a credential's reach.
47
- export const FORMAT = 4;
44
+ // FORMAT is the scan and approved-selection format. 5: proxy-only Secrets, hosts from
45
+ // TOKEN_HOSTS, GitHub tokens also for github.com (git over HTTPS). Older files approved env
46
+ // delivery or fewer hosts; they are refused, never reinterpreted, so an approval cannot widen a
47
+ // credential's reach.
48
+ export const FORMAT = 5;
48
49
 
49
50
  export const TOKEN_HOSTS = new Map(
50
51
  Object.entries({
51
- GH_TOKEN: ['api.github.com'],
52
- GITHUB_TOKEN: ['api.github.com'],
52
+ GH_TOKEN: ['api.github.com', 'github.com'],
53
+ GITHUB_TOKEN: ['api.github.com', 'github.com'],
53
54
  ANTHROPIC_API_KEY: ['api.anthropic.com'],
54
55
  OPENAI_API_KEY: ['api.openai.com'],
55
56
  GEMINI_API_KEY: ['generativelanguage.googleapis.com'],
@@ -409,7 +409,7 @@ function main() {
409
409
  );
410
410
  if (selection.leftover?.length)
411
411
  console.log(
412
- `Previously saved and left unchanged: ${selection.leftover.join(' ')}. Remove only if intended: instabox secrets rm NAME --org ORG.`,
412
+ `Previously saved and left unchanged: ${selection.leftover.join(' ')}. Remove only if intended: instabox secrets rm --yes NAME --org ORG.`,
413
413
  );
414
414
  process.exit(result.code);
415
415
  }
@@ -139,7 +139,7 @@ function scanTokens({ home, env, procEnv }) {
139
139
  manual.push({
140
140
  what: 'GitHub token (GH_TOKEN)',
141
141
  why: hasCommand('gh', env) ? 'gh is not signed in on this machine' : 'gh is not installed on this machine',
142
- todo: 'sign in here with `gh auth login` and run this again; or put a token in yourself: `instabox secrets set GH_TOKEN --org <org> --host api.github.com` (it prompts without echo)',
142
+ todo: 'sign in here with `gh auth login` and run this again; or put a token in yourself: `instabox secrets set GH_TOKEN --org <org> --host api.github.com --host github.com` (it prompts without echo)',
143
143
  });
144
144
  manual.push({
145
145
  what: 'Claude Code sign-in',
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: instabox-snapshots
3
- description: Use when the user wants to save an instabox box's state and load it later — "save a snapshot", "checkpoint this box", "restore/load from a snapshot", "start a new box from my setup", "clone this box". Covers instabox snapshot save/ls/files/rm and create --snapshot.
3
+ description: Use when the user wants to save an instabox box's state and load it later — "save a snapshot", "checkpoint this box", "restore/load from a snapshot", "start a new box from my setup", "clone this box". Covers instabox snapshot save/ls/files/rm, create --snapshot and fork.
4
4
  metadata:
5
5
  installed-by: instabox
6
6
  ---
@@ -42,12 +42,19 @@ This reads the snapshot's root disk only; `/data` (and HOME) are not browsable h
42
42
 
43
43
  ## Copy a box
44
44
 
45
- There is no direct copy: save a snapshot of it, create the new box from that, and delete the snapshot if you will not use it again.
45
+ `instabox fork` does it in one command: it saves a snapshot of the box, creates the new box (or `--count` up to 5, named `NAME-1` to `NAME-N`) from it with the same template and size, waits until it runs, then deletes the snapshot (also on error or Ctrl-C) unless `--keep-snapshot`. It is the same snapshot save + create, so it takes about as long; the source box is not changed.
46
+
47
+ ```sh
48
+ instabox fork work --name work2
49
+ instabox fork work --name try --count 3 --json # try-1, try-2, try-3
50
+ ```
51
+
52
+ The same by hand, when you want the snapshot's name or the copy's create flags (`--secrets`, `--no-vault`, `--auto-suspend`):
46
53
 
47
54
  ```sh
48
55
  instabox snapshot save work --name work-copy
49
56
  instabox create --name work2 --snapshot work-copy
50
- instabox snapshot rm work-copy # optional; work2 is not affected
57
+ instabox snapshot rm --yes work-copy # optional; work2 is not affected
51
58
  ```
52
59
 
53
60
  The copy gets a new sandbox identity and the Secret selection in its creation request. Without `--secrets` it keeps the existing Vault behavior and does not enroll in org Secrets; it does not inherit the source's selection. To give it neither store's credentials, use:
@@ -61,7 +68,7 @@ To give the copy API credentials, name them with `--secrets NAME,NAME`. Placehol
61
68
  ## Clean up
62
69
 
63
70
  ```sh
64
- instabox snapshot rm work-ready
71
+ instabox snapshot rm --yes work-ready
65
72
  ```
66
73
 
67
74
  Snapshots count toward the account's snapshot and storage limits (`instabox usage`). Delete ones you no longer need.
@@ -71,4 +78,4 @@ Snapshots count toward the account's snapshot and storage limits (`instabox usag
71
78
  1. `instabox create --name job --snapshot <base>` (it pauses by itself after 30 idle minutes)
72
79
  2. Work with `instabox exec job -- ...`
73
80
  3. When the setup is worth keeping: `instabox snapshot save job --name <base>-v2` (no need to stop it)
74
- 4. `instabox rm job` when done.
81
+ 4. `instabox rm --yes job` when done.