instabox 0.6.3 → 0.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
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,7 +18,7 @@ instabox login # confirm the browser's account, or choose GitHub /
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` open account confirmation in the browser: use its current Instabox account or choose another. To go directly to a provider, use `instabox login --provider github|google`, or `npx instabox@latest --github|--google`. The CLI shares the sign-in `npx` made. Device-code sign-in (`--device`) uses GitHub.
20
20
 
21
- For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it on the dashboard's API Keys page). Never print, commit or log a key.
21
+ 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: Profile → API keys (account menu)). Never print, commit or log a key.
22
22
 
23
23
  ## Templates
24
24
 
@@ -27,6 +27,7 @@ For CI or an agent without a browser, use an API key instead: `export INSTABOX_A
27
27
  - `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
28
  - `herdr`: for the herdr client.
29
29
  - `orca`: for Orca desktop and mobile.
30
+ - `t3`: for the T3 Code desktop and mobile clients.
30
31
 
31
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`.
32
33
 
@@ -52,7 +53,9 @@ instabox ssh work # interactive shell (leave with:
52
53
  instabox exec work -- uname -a # one command, no SSH client needed
53
54
  instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
54
55
  instabox files work /data/home # list a directory (or print a text file)
55
- instabox auto-suspend work 10m # idle time before it pauses (fixed at 30m unless an admin gave the account manual lifecycle)
56
+ instabox auto-suspend work 10m # idle time before a paid box pauses
57
+ instabox max-run work 120 # pause a paid box after two running hours
58
+ instabox max-run work -1 # remove the runtime cap; idle policy still applies
56
59
  instabox stop work && instabox start work # stop ends processes, keeps disks
57
60
  instabox suspend work && instabox resume work # pause/resume keeps running processes
58
61
  instabox rename work api # rename in place, any state; ID and port URLs stay
@@ -60,41 +63,43 @@ instabox rm work # delete the box
60
63
  ```
61
64
 
62
65
  - Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
63
- - `create` options: `--template T` or `--snapshot S`, `--vcpus N --mem-mib M` (a size of the plan, see Sizes), `--auto-suspend D|0`, `--meta K=V`, `--org O`, `--secrets all|NAME,NAME|none`, `--env-secrets NAME,NAME|none`, `--no-wait`.
64
- - A box pauses itself after 30 idle minutes (no agent working, no terminal use, little CPU) and wakes on the next `exec`, `ssh`, `files`, public port request or `instabox wake <box>`; a paused box keeps its memory and processes. The 30 minutes are fixed for every account unless an admin gave it manual lifecycle: only then do `--auto-suspend 0` (always on) and `--auto-suspend 2h` apply, and the CLI prints a note when the server kept 30m. Boxes have no other timeout. Do not try to keep a box on by other means; a paused box resumes on the next command.
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`.
67
+ - 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
+ - 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
+ - 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.
65
70
  - Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
66
- - `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 anything longer (installs, builds, test suites) over SSH, which has no time limit and also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`.
71
+ - `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.
67
72
  - 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.
68
73
  - 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.
69
74
 
70
75
  ## Organization Secrets
71
76
 
72
- Secrets are encrypted key=value entries owned by an org. Each entry has one delivery mode:
77
+ 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.
73
78
 
74
- - `proxy`: the box receives a placeholder. The outbound proxy replaces it in ordinary HTTPS header values only when the destination matches the Secret's allowed hosts. Code in the box can use this credential but does not receive its real value through Secret delivery. No header-name configuration is needed.
75
- - `env`: the box receives the real value. Use this explicitly for native database credentials, local signing, and clients that cannot use header substitution. Code running in the box can read and copy the value.
79
+ 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.
76
80
 
77
- A new box without Secret selection flags keeps the existing Vault behavior. Opt into Secrets with `--secrets` or `--env-secrets`, or use `secrets select` later. At creation, `--env-secrets` alone also selects all org proxy Secrets. Select env names explicitly; there is no env `all` mode. Metadata labels do not select Secrets, and Secrets do not deliver files.
81
+ 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.
78
82
 
79
83
  ```sh
80
84
  instabox secrets ls --org team # metadata only
81
- instabox secrets show work # selection and independent proxy/guest status
82
- instabox secrets set SERVICE_API_KEY --org team --delivery proxy --host api.example.com
83
- instabox secrets set DATABASE_URL --org team --delivery env
84
- instabox create --name app --org team --secrets SERVICE_API_KEY --env-secrets DATABASE_URL
85
- instabox secrets select work --env-secrets DATABASE_URL
86
- instabox secrets push work # refresh managed env/trust on a running box
87
- instabox create --name tenant --no-vault --secrets none --env-secrets none
85
+ instabox secrets show work # selection and proxy status
86
+ instabox secrets set SERVICE_API_KEY --org team --host api.example.com
87
+ instabox secrets import .env --org team --host api.example.com # every value gets these hosts
88
+ instabox create --name app --org team --secrets SERVICE_API_KEY
89
+ instabox secrets select work --secrets SERVICE_API_KEY,OTHER_API_KEY
90
+ instabox secrets push work # refresh the selection on a running box
91
+ instabox create --name tenant --no-vault --secrets none
88
92
  ```
89
93
 
90
- - Values for `set` come from stdin or a hidden prompt. Never print a credential or put its value into agent output, chat, command arguments, or logs. Before asking for a credential, check metadata with `secrets ls --org <org>` and `secrets show <box>`. A silent check such as `test "${NAME+x}" = x` tests variable presence without revealing it; it does not prove successful proxy authorization.
91
- - 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, delivery-mode 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.
92
- - Selection updates replace each supplied list; omitted lists remain unchanged. Preserve existing names when adding to a selection. Proxy selection accepts `all`; both selections accept `none` or `[]`. JSON arrays allow a Secret literally named `all` or `none`.
93
- - Proxy delivery requires allowed hosts and works with literal placeholders in HTTPS headers on port 443. Do not hash, encode, or sign with a placeholder. Native database TLS is not HTTPS. Unsupported traffic never falls back to real-value env delivery.
94
- - Rotating an existing proxy value preserves its placeholder. Env refresh and newly added variables affect new shells that load the managed environment; restart or reconfigure existing applications and connection pools. Removing a selection cannot recover env values already copied into processes, files, or snapshots. Revoke or rotate those credentials at the upstream service when necessary.
95
- - For a box intended for another person or tenant, disable personal Vault injection with `--no-vault` and explicitly select only the org Secrets they may use. For a fresh box with neither system's credentials, use `--no-vault --secrets none --env-secrets none`. Empty Secret selections alone still permit Vault injection. These flags do not remove credentials already copied into a source snapshot.
96
- - For an existing box, disable Vault with `start BOX --no-vault` or `resume BOX --no-vault` and explicitly clear both Secret selections with `secrets select BOX --secrets none --env-secrets none`. Refresh a running guest with both `vault push BOX` and `secrets push BOX`. Existing processes and copied files can retain values; disabling either system does not recover those copies.
97
- - Vault remains available through `vault set`, `set-file`, `import`, `ls`, `rm`, `show`, and `push`. The CLI warns about future retirement. A box may use Vault and Secrets together; Secrets wins same-named environment variables. Neither store automatically copies or deletes the other. Move values deliberately when the user is ready.
94
+ - Values for `set` come from stdin or a hidden prompt. Never print a credential or put its value into agent output, chat, command arguments, or logs. Before asking for a credential, check metadata with `secrets ls --org <org>` and `secrets show <box>`. A silent check such as `test "${NAME+x}" = x` tests that the placeholder is present without revealing it; it does not prove the upstream accepts the credential.
95
+ - `--host` is required and repeatable: give each exact hostname the credential may be sent to (no scheme, port, path, wildcard or IP address). Use the service's API host, such as `api.openai.com`, never a guess from the credential's value.
96
+ - 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.
97
+ - `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.
98
+ - 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.
99
+ - 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.
100
+ - 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.
101
+ - 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.
102
+ - 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.
98
103
 
99
104
  ## Moving work onto a box
100
105
 
@@ -1,19 +1,21 @@
1
1
  ---
2
2
  name: instabox-basic-setup
3
- description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves selected credentials as org key=value Secrets with explicit proxy/env modes, grants them only through sandbox selections, and copies approved ordinary config files directly to named running sandboxes. It does not put files in the Secret store or automatically give real credentials to future boxes. Moves no code; for a repo, use instabox-move-repo.
3
+ description: Use when the user wants their instabox setup to use local tokens, git config or editor config — "put my tokens / dotfiles / git config into instabox", "set up my box like my laptop", "move my setup to instabox". Produces a names-only scan and a reviewable plan, saves known API tokens as org Secrets for their service's HTTPS API hosts, selects them on named sandboxes, and copies approved ordinary config files directly to named running sandboxes. It does not put files in the Secret store. Moves no code; for a repo, use instabox-move-repo.
4
4
  metadata:
5
5
  installed-by: instabox
6
6
  ---
7
7
 
8
8
  # Basic setup with org Secrets and sandbox config copies
9
9
 
10
- Use the user's chosen org and sandbox IDs. Secrets are shared within an org, but real-value `env` delivery requires explicit sandbox selection. A `proxy` Secret supplies a placeholder for HTTPS header substitution at its allowed hosts. Other org sandboxes selecting all proxy Secrets, including future sandboxes, can also use a newly saved proxy credential. Updating an existing Secret affects every sandbox already authorized to use it.
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
+
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.
11
13
 
12
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.
13
15
 
14
16
  | Group | Destination | Sources |
15
17
  | --- | --- | --- |
16
- | Tokens | Encrypted org key=value Secrets; explicit per-token `proxy` or `env` mode | GitHub token through `gh auth token`; known service credentials in the environment or shell startup files; other credential-like exports listed unchecked |
18
+ | Tokens | Encrypted org Secrets, each limited to its registry hosts | GitHub token through `gh auth token`; registry tokens in the environment or shell startup files; other credential-like exports listed as manual steps |
17
19
  | Git | Files under the selected sandbox's home | `.gitconfig`, `.config/git/config`, global gitignore |
18
20
  | Editor and terminal | Files under the selected sandbox's home | `.vimrc`, nvim config without plugin checkouts, `.tmux.conf` |
19
21
 
@@ -23,11 +25,11 @@ The scripts beside this file require Node 18+ locally and Python 3 in sandboxes
23
25
  | --- | --- |
24
26
  | `scan.mjs` | Read-only inventory of names, source locations, sizes and manual setup requirements; no credential values in output |
25
27
  | `plan.mjs` | Write and strictly parse the editable plan, binding it to an org and Secret metadata |
26
- | `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets through CLI stdin, merge explicit sandbox selections, and copy config |
28
+ | `push.mjs` | Read approved values into memory, validate all inputs and quotas, save Secrets with their registry hosts through CLI stdin, merge sandbox selections, and copy config |
27
29
  | `content.mjs` | Rewrite HOME paths, remove recognized credential lines, and adapt git/editor configuration for Linux |
28
30
  | `install-files.py` | Install ordinary config files privately, refusing symlinks and unsafe paths; no Secret-store or legacy Vault access |
29
31
 
30
- Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through a hidden CLI prompt with the appropriate org and delivery mode.
32
+ Never print credential values, shell startup files, credential-bearing config, or `gh auth token` output. Do not run these scripts against the user's machine unless their request authorizes this setup. Scripts may read credentials internally; their output is limited to names, sizes and outcomes. If a value cannot be obtained, let the user supply it through the hidden prompt of `instabox secrets set NAME --org ORG --host HOST`.
31
33
 
32
34
  ## 1. Scan and choose the org
33
35
 
@@ -53,20 +55,11 @@ node "$S/plan.mjs" write --scan "$W/scan.json" --secrets "$W/secrets.json" \
53
55
  --org "$ORG_ID" --sandbox <sandbox-id> > "$W/plan.md"
54
56
  ```
55
57
 
56
- Repeat `--sandbox` for additional targets, or omit it to save Secrets without adding env grants or copying files. The plan lists `Org`, `Sandboxes`, checkboxes and one `Delivery` line per token. The org is bound to the metadata used to generate the plan; regenerate for another org. Sandbox IDs and delivery policies can be edited in the plan.
57
-
58
- A token policy looks like either of these:
59
-
60
- ```text
61
- Delivery `env:SERVICE_API_KEY`: `proxy` hosts `api.example.com`
62
- Delivery `env:DATABASE_URL`: `env` hosts `-`
63
- ```
64
-
65
- The `env:` prefix is the scanner's variable ID, not its delivery mode. New token rows start with explicit `env` mode for client compatibility; this gives the named sandboxes the real value. Choose `proxy` and exact approved hosts when the client sends the value literally in an HTTPS header on port 443. Do not infer hosts from a token's value or silently switch modes when a client fails. Native database protocols, local signing and git's encoded credential exchange need the real value or a separate protocol-aware broker. Proxy placeholders cannot be used as real signing or encoding inputs.
58
+ Repeat `--sandbox` for additional targets, or omit it to save Secrets without changing any selection or copying files. The plan lists `Org`, `Sandboxes` and checkboxes; each token row names the hosts its Secret is limited to. The org is bound to the metadata used to generate the plan; regenerate for another org. Sandbox IDs and checkboxes can be edited in the plan; hosts come from the registry and cannot.
66
59
 
67
- Well-known token rows are checked by default; unknown and keychain-backed sources are unchecked. Existing Secrets changed outside the last successful run are unchecked, and checking them authorizes replacement in the org. A previous proxy mode is preserved as the starting policy on a repeat run. Files are unchecked when no sandbox is selected. Checking a config file authorizes replacement of that path on each named sandbox.
60
+ The `env:` prefix in a row ID is the scanner's variable ID. Registry token rows are checked by default; keychain-backed sources are unchecked. Existing Secrets changed outside the last successful run are unchecked, and checking them authorizes replacement in the org. Files are unchecked when no sandbox is selected. Checking a config file authorizes replacement of that path on each named sandbox.
68
61
 
69
- Show the concrete names, modes, allowed hosts, org and target sandboxes. Obtain any choices or authorization the original request did not already provide before reading values or writing. Do not ask again for an action already explicitly authorized. Explain that env values are readable by sandbox code and may persist in processes, files or snapshots; removing a selection cannot recover them.
62
+ Show the concrete names, allowed hosts, org and target sandboxes. Obtain any choices or authorization the original request did not already provide before reading values or writing. Do not ask again for an action already explicitly authorized.
70
63
 
71
64
  Parse the approved plan:
72
65
 
@@ -75,7 +68,7 @@ node "$S/plan.mjs" parse --scan "$W/scan.json" --secrets "$W/secrets.json" \
75
68
  --org "$ORG_ID" --limits "$W/limits.json" "$W/plan.md" > "$W/selection.json"
76
69
  ```
77
70
 
78
- Exit 1 identifies malformed or missing rows, invalid policies, a changed org/catalog, unsafe config, or a file selection without targets. Fix choices without changing item IDs or deleting rows. Exit 3 identifies quota or copy-size limits. Config files have their own copy limits and do not count as Secret entries. Values are measured before push; plan generation never reads token values. Legacy selector-based plans and manifests grant no authority in the new model and must be regenerated.
71
+ Exit 1 identifies malformed or missing rows, a changed org/catalog, a scan with a token outside the registry, unsafe config, or a file selection without targets. Fix choices without changing item IDs or deleting rows. Exit 3 identifies quota or copy-size limits. Config files have their own copy limits and do not count as Secret entries. Values are measured before push; plan generation never reads token values. Legacy selector-based plans and manifests grant no authority in the new model and must be regenerated.
79
72
 
80
73
  ## 3. Dry-run, then apply the approved selection
81
74
 
@@ -88,11 +81,11 @@ The dry-run reads the approved credential sources and prepares filtered config,
88
81
 
89
82
  The pusher verifies org ownership, target orgs, Secret IDs and versions, input limits and total Secret quota before mutations. If a selected Secret changed since planning, regenerate the plan. These are preflight checks, not a transaction across all CLI operations: concurrent org edits can still occur during application. A failed run reports partial outcomes; successfully saved Secrets remain in the org. Do not assume rollback or automatic removal of previously copied files.
90
83
 
91
- Only selected token names are added to each target's grants; existing selections are preserved, including proxy `all`. Env selection never becomes `all`. Running targets receive an immediate managed-environment refresh; stopped targets can receive selections for their next start, but config copying requires them to be running. A failed or pending guest refresh does not install config that may depend on those credentials. Existing processes need to reload their environment or restart.
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.
92
85
 
93
- 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. The gh git helper is added only when this plan explicitly selects `GH_TOKEN` with env delivery. An existing org Secret by itself is not a sandbox grant; reuse it through an explicitly reviewed `instabox secrets select` update if needed, preserving 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: 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.
94
87
 
95
- The local manifest at `~/.config/instabox-basic-setup/pushed.json` stores org IDs, Secret IDs/versions/modes 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.
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.
96
89
 
97
90
  ## 4. Verify and report
98
91
 
@@ -101,8 +94,8 @@ instabox secrets show <box>
101
94
  instabox exec <box> -- 'test "${GH_TOKEN+x}" = x && test -f "$HOME/.gitconfig"'
102
95
  ```
103
96
 
104
- Use silent presence checks and metadata; never print variables to verify them. A placeholder's presence does not prove proxy authorization or upstream success. Confirm client behavior without logging credentials, and explain application restarts when env values changed.
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.
105
98
 
106
- Report the org Secret names and modes saved, sandbox IDs whose selections changed, config paths copied, failures and manual setup steps. Saving env Secrets without targets adds no grants; replacing a value still affects sandboxes already authorized to use it. Future boxes without Secret selection flags keep their existing Vault behavior. At creation, selecting env Secrets alone also selects all org proxy Secrets; add an explicit proxy selection to narrow that access. Previously saved credentials or config not selected this time remain; deletion or cleanup needs its own intended scope. Rotate or revoke upstream when a previously delivered env value must stop working.
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.
107
100
 
108
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.
@@ -1,7 +1,7 @@
1
1
  // What a directly copied configuration file becomes: the local file with this
2
2
  // machine's HOME rewritten to the box's, lines that look like credentials
3
3
  // removed and macOS-only lines commented out. ~/.gitconfig also gets a few
4
- // changes so git works on a box that has GH_TOKEN and no SSH key.
4
+ // changes so git works on a box that has no signing or SSH key.
5
5
  //
6
6
  // Used by plan.mjs (for sizes) and push.mjs (for the upload). Nothing here
7
7
  // prints a file's content.
@@ -40,25 +40,11 @@ export function scrubText(text, { path, home }) {
40
40
  return { text: out.join('\n'), secrets, macos };
41
41
  }
42
42
 
43
- // The credential helper `gh auth setup-git` writes: git over HTTPS uses GH_TOKEN through gh.
44
- export const GH_HELPER = [
45
- '',
46
- `# ${TAG}: git over HTTPS to GitHub uses an explicitly selected env GH_TOKEN through gh`,
47
- '[credential "https://github.com"]',
48
- '\thelper =',
49
- '\thelper = !gh auth git-credential',
50
- '[credential "https://gist.github.com"]',
51
- '\thelper =',
52
- '\thelper = !gh auth git-credential',
53
- '',
54
- ].join('\n');
55
-
56
- // ~/.gitconfig for the box. ghToken: GH_TOKEN is explicitly selected for env delivery, so the gh helper is added.
57
- // Changes, besides scrubText's:
43
+ // ~/.gitconfig for the box. Changes, besides scrubText's:
58
44
  // - commit/tag signing is turned off: the signing key (GPG, SSH, 1Password) is not on the box;
59
45
  // - url.<ssh>.insteadOf rewrites are commented out: the box has no SSH key, and they would
60
46
  // send HTTPS remotes over SSH.
61
- export function gitconfigText(text, { home, ghToken }) {
47
+ export function gitconfigText(text, { home }) {
62
48
  const r = scrubText(text, { path: '.gitconfig', home });
63
49
  let section = '';
64
50
  let signing = 0;
@@ -77,13 +63,11 @@ export function gitconfigText(text, { home, ghToken }) {
77
63
  }
78
64
  return line;
79
65
  });
80
- let out = lines.join('\n');
81
- if (ghToken) out = out.replace(/\n*$/, '\n') + GH_HELPER;
82
- return { text: out, secrets: r.secrets, macos: r.macos, signing, insteadOf };
66
+ return { text: lines.join('\n'), secrets: r.secrets, macos: r.macos, signing, insteadOf };
83
67
  }
84
68
 
85
69
  // The content of one file of a file item, or {error}. f: {path, src, transform}.
86
- export function fileContent(f, { home, ghToken = false } = {}) {
70
+ export function fileContent(f, { home } = {}) {
87
71
  let buf;
88
72
  try {
89
73
  buf = readFileSync(f.src);
@@ -93,7 +77,7 @@ export function fileContent(f, { home, ghToken = false } = {}) {
93
77
  if (buf.includes(0)) return { error: `${f.src}: a binary file (config copying supports UTF-8 text only)` };
94
78
  const text = buf.toString('utf8');
95
79
  if (!Buffer.from(text).equals(buf)) return { error: `${f.src}: invalid UTF-8 text` };
96
- if (f.transform === 'gitconfig') return gitconfigText(text, { home, ghToken });
80
+ if (f.transform === 'gitconfig') return gitconfigText(text, { home });
97
81
  if (f.transform === 'raw') {
98
82
  if (SECRET_VALUE.test(text) || text.split('\n').some((line) => SECRET_KEY_LINE.test(line)))
99
83
  return { error: `${f.src}: possible credential in structured config; review it separately` };
@@ -3,7 +3,6 @@
3
3
  // The one rule every function here keeps: a credential's value is never
4
4
  // returned to the agent, stored in a file or printed. Code that looks at a
5
5
  // secret returns names, places and sizes only.
6
- import { isIP } from 'node:net';
7
6
  import { spawnSync } from 'node:child_process';
8
7
  import { lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
9
8
  import { fileURLToPath } from 'node:url';
@@ -38,50 +37,55 @@ export const SECRET_VALUE = new RegExp(
38
37
  export const SECRET_KEY_LINE =
39
38
  /(?:^|[\s{,;:])["']?[\w.-]*(?:token|password|passwd|secret|api[_-]?key|apikey|_auth|authorization|extraheader|private[_-]?key)[\w.-]*["']?\s*[=:]\s*["']?(?![0-9.]+\b|true\b|false\b)[^\s"',}]/i;
40
39
 
41
- // Model-provider and developer-service tokens: moved by default when found. Any other
42
- // credential-looking variable exported in a shell file is listed unchecked.
43
- export const WELL_KNOWN = new Set([
44
- 'GH_TOKEN',
45
- 'GITHUB_TOKEN',
46
- 'CLAUDE_CODE_OAUTH_TOKEN',
47
- 'ANTHROPIC_API_KEY',
48
- 'OPENAI_API_KEY',
49
- 'GEMINI_API_KEY',
50
- 'GOOGLE_API_KEY',
51
- 'GOOGLE_GENERATIVE_AI_API_KEY',
52
- 'OPENROUTER_API_KEY',
53
- 'GROQ_API_KEY',
54
- 'MISTRAL_API_KEY',
55
- 'DEEPSEEK_API_KEY',
56
- 'XAI_API_KEY',
57
- 'TOGETHER_API_KEY',
58
- 'FIREWORKS_API_KEY',
59
- 'PERPLEXITY_API_KEY',
60
- 'COHERE_API_KEY',
61
- 'VOYAGE_API_KEY',
62
- 'REPLICATE_API_TOKEN',
63
- 'ELEVENLABS_API_KEY',
64
- 'TAVILY_API_KEY',
65
- 'EXA_API_KEY',
66
- 'FIRECRAWL_API_KEY',
67
- 'BRAVE_API_KEY',
68
- 'PINECONE_API_KEY',
69
- 'HF_TOKEN',
70
- 'HUGGING_FACE_HUB_TOKEN',
71
- 'NPM_TOKEN',
72
- 'VERCEL_TOKEN',
73
- 'NETLIFY_AUTH_TOKEN',
74
- 'CLOUDFLARE_API_TOKEN',
75
- 'SUPABASE_ACCESS_TOKEN',
76
- 'FLY_API_TOKEN',
77
- 'RAILWAY_TOKEN',
78
- 'RENDER_API_KEY',
79
- 'NEON_API_KEY',
80
- 'SENTRY_AUTH_TOKEN',
81
- 'LINEAR_API_KEY',
82
- 'GITLAB_TOKEN',
83
- 'DIGITALOCEAN_ACCESS_TOKEN',
84
- ]);
40
+ // The tokens this skill saves, each with the HTTPS API hosts its usual client sends it to in a
41
+ // request header. The box gets a placeholder and the outbound proxy fills in the token only for
42
+ // these hosts. A token whose client signs with it, sends it in a body or URL, or talks to hosts
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;
48
+
49
+ export const TOKEN_HOSTS = new Map(
50
+ Object.entries({
51
+ GH_TOKEN: ['api.github.com'],
52
+ GITHUB_TOKEN: ['api.github.com'],
53
+ ANTHROPIC_API_KEY: ['api.anthropic.com'],
54
+ OPENAI_API_KEY: ['api.openai.com'],
55
+ GEMINI_API_KEY: ['generativelanguage.googleapis.com'],
56
+ GOOGLE_API_KEY: ['generativelanguage.googleapis.com'],
57
+ GOOGLE_GENERATIVE_AI_API_KEY: ['generativelanguage.googleapis.com'],
58
+ OPENROUTER_API_KEY: ['openrouter.ai'],
59
+ GROQ_API_KEY: ['api.groq.com'],
60
+ MISTRAL_API_KEY: ['api.mistral.ai'],
61
+ DEEPSEEK_API_KEY: ['api.deepseek.com'],
62
+ XAI_API_KEY: ['api.x.ai'],
63
+ TOGETHER_API_KEY: ['api.together.ai', 'api.together.xyz'],
64
+ FIREWORKS_API_KEY: ['api.fireworks.ai'],
65
+ PERPLEXITY_API_KEY: ['api.perplexity.ai'],
66
+ COHERE_API_KEY: ['api.cohere.ai', 'api.cohere.com'],
67
+ VOYAGE_API_KEY: ['api.voyageai.com'],
68
+ REPLICATE_API_TOKEN: ['api.replicate.com'],
69
+ ELEVENLABS_API_KEY: ['api.elevenlabs.io'],
70
+ TAVILY_API_KEY: ['api.tavily.com'],
71
+ EXA_API_KEY: ['api.exa.ai'],
72
+ FIRECRAWL_API_KEY: ['api.firecrawl.dev'],
73
+ BRAVE_API_KEY: ['api.search.brave.com'],
74
+ HF_TOKEN: ['huggingface.co', 'router.huggingface.co'],
75
+ HUGGING_FACE_HUB_TOKEN: ['huggingface.co', 'router.huggingface.co'],
76
+ NPM_TOKEN: ['registry.npmjs.org'],
77
+ VERCEL_TOKEN: ['api.vercel.com'],
78
+ NETLIFY_AUTH_TOKEN: ['api.netlify.com'],
79
+ CLOUDFLARE_API_TOKEN: ['api.cloudflare.com'],
80
+ SUPABASE_ACCESS_TOKEN: ['api.supabase.com'],
81
+ RENDER_API_KEY: ['api.render.com'],
82
+ NEON_API_KEY: ['console.neon.tech'],
83
+ SENTRY_AUTH_TOKEN: ['sentry.io'],
84
+ LINEAR_API_KEY: ['api.linear.app'],
85
+ GITLAB_TOKEN: ['gitlab.com'],
86
+ DIGITALOCEAN_ACCESS_TOKEN: ['api.digitalocean.com'],
87
+ }),
88
+ );
85
89
 
86
90
  // Reserved Secret variable names (internal/secrets/validate.go).
87
91
  const RESERVED = new Set([
@@ -206,28 +210,6 @@ export function configPathOK(p) {
206
210
  return p.split('/').every((c) => c !== '' && c !== '.' && !c.startsWith('..'));
207
211
  }
208
212
 
209
- export function deliveryPolicy(delivery, hosts = []) {
210
- if (!['env', 'proxy'].includes(delivery) || !Array.isArray(hosts)) throw new Error('delivery must be env or proxy');
211
- if (delivery === 'env' && hosts.length) throw new Error('env delivery cannot enforce hosts; use -');
212
- if (delivery === 'proxy' && (!hosts.length || hosts.length > 32))
213
- throw new Error('proxy delivery requires 1-32 exact hosts');
214
- const normalized = hosts.map((name) => {
215
- if (typeof name !== 'string') throw new Error('hosts must be exact public DNS names');
216
- const h = name.toLowerCase().replace(/\.$/, '');
217
- if (
218
- h.length > 253 ||
219
- !h.includes('.') ||
220
- isIP(h) ||
221
- /(?:^|\.)(?:local|localhost)$/.test(h) ||
222
- h.split('.').some((label) => !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label))
223
- ) {
224
- throw new Error('hosts must be exact public DNS names without a scheme, port, path, IP address or wildcard');
225
- }
226
- return h;
227
- });
228
- return { delivery, hosts: [...new Set(normalized)].sort() };
229
- }
230
-
231
213
  export function validID(s) {
232
214
  return typeof s === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(s);
233
215
  }
@@ -237,7 +219,6 @@ export function secretIdentity(s) {
237
219
  name: s.name,
238
220
  orgId: s.orgId,
239
221
  version: s.version,
240
- delivery: s.delivery,
241
222
  hosts: [...(s.hosts || [])].sort(),
242
223
  };
243
224
  }
@@ -11,7 +11,8 @@ import {
11
11
  FILE_MAX_VALUE,
12
12
  FILE_MAX_TOTAL,
13
13
  configPathOK,
14
- deliveryPolicy,
14
+ FORMAT,
15
+ TOKEN_HOSTS,
15
16
  human,
16
17
  isMain,
17
18
  sameSecret,
@@ -60,30 +61,29 @@ export class PlanError extends Error {
60
61
  }
61
62
 
62
63
  function checkContext(scan, secrets, orgId) {
63
- if (scan.version !== 3 || !Array.isArray(scan.items))
64
+ if (
65
+ scan.version !== FORMAT ||
66
+ !Array.isArray(scan.items) ||
67
+ scan.items.some((it) => it.kind === 'env' && !TOKEN_HOSTS.has(it.name))
68
+ )
64
69
  throw new PlanError(['scan format changed; run scan.mjs again']);
65
70
  if (!validID(orgId)) throw new PlanError(['an explicit org ID is required']);
66
71
  if (!Array.isArray(secrets) || secrets.some((s) => s.orgId !== orgId || !s.id || !Number.isSafeInteger(s.version)))
67
72
  throw new PlanError(['Secret metadata must belong to the selected org and include IDs and versions']);
68
73
  }
69
74
 
70
- export function writePlan(
71
- scan,
72
- { secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [], delivery = 'env', hosts = [] } = {},
73
- ) {
75
+ export function writePlan(scan, { secrets = [], manifest = { secrets: [] }, orgId, sandboxes = [] } = {}) {
74
76
  checkContext(scan, secrets, orgId);
75
77
  if (sandboxes.some((id) => !validID(id) || !id.startsWith('m_')))
76
78
  throw new PlanError(['sandboxes must use stable m_ IDs']);
77
- const policy = deliveryPolicy(delivery, hosts);
78
79
  const out = [
79
80
  '# instabox basic setup: plan',
80
81
  '',
81
82
  'Check `[x]` the items to save or copy. Do not change IDs or delete lines.',
82
- 'Tokens become encrypted org Secrets. Each Delivery line explicitly chooses env (real values) or proxy (HTTPS-header placeholders with allowed hosts).',
83
- 'This run adds env grants only to the sandbox IDs below; an empty list adds no grants. Existing grants remain in force.',
84
- 'Proxy Secrets also become available to other org sandboxes whose proxy selection is all, including future sandboxes.',
83
+ 'Tokens become encrypted org Secrets. A box sees a placeholder; the outbound proxy puts the token into HTTPS headers sent to the hosts listed for each token, and nowhere else.',
84
+ 'This run selects them only on the sandbox IDs below; an empty list changes no selection. Existing selections remain in force.',
85
+ 'Saved Secrets also become available to other org sandboxes whose selection is all, including future sandboxes.',
85
86
  'Replacing an existing Secret affects its other authorized sandboxes. Config files are copied once to the named running sandboxes and may overwrite those paths; they are not Secrets.',
86
- 'Env values can be copied into files, processes or snapshots; removing a selection cannot recover those copies.',
87
87
  '',
88
88
  `Org: \`${orgId}\``,
89
89
  `Sandboxes: \`${[...new Set(sandboxes)].join(',') || '-'}\``,
@@ -104,7 +104,7 @@ export function writePlan(
104
104
  const checked = it.checked && !other && (it.kind === 'env' || sandboxes.length > 0);
105
105
  let description =
106
106
  it.kind === 'env'
107
- ? `${it.name} from ${it.from}`
107
+ ? `${it.name} from ${it.from}, for ${TOKEN_HOSTS.get(it.name).join(', ')}`
108
108
  : `~/${it.path}: ${it.files.length} file(s), ${human(it.bytes)}`;
109
109
  if (it.reason) description += `; off by default: ${it.reason}`;
110
110
  if (it.note) description += `; ${it.note}`;
@@ -112,12 +112,6 @@ export function writePlan(
112
112
  else if (ex.length) description += '; replaces the unchanged Secret from your previous run';
113
113
  if (it.kind === 'file' && !sandboxes.length) description += '; select running sandbox IDs before checking this';
114
114
  out.push(`- [${checked ? 'x' : ' '}] \`${it.id}\` ${description}`);
115
- if (it.kind === 'env') {
116
- // Preserve an existing mode as a starting point; never silently change
117
- // a proxy credential into a real-value env credential on a repeat run.
118
- const p = ex.length ? deliveryPolicy(ex[0].item.delivery, ex[0].item.hosts || []) : policy;
119
- out.push(`Delivery \`${it.id}\`: \`${p.delivery}\` hosts \`${p.hosts.join(',') || '-'}\``);
120
- }
121
115
  }
122
116
  }
123
117
  out.push('', '## Manual setup', '', 'These entries have no checkbox; no credentials are read for them.', '');
@@ -129,8 +123,7 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
129
123
  checkContext(scan, secrets, orgId);
130
124
  const errors = [];
131
125
  const byId = new Map(scan.items.map((it) => [it.id, it]));
132
- const states = new Map(),
133
- policies = new Map();
126
+ const states = new Map();
134
127
  let planOrg, sandboxes, digest;
135
128
  for (const [index, raw] of text.split('\n').entries()) {
136
129
  const line = raw.replace(/\r$/, ''),
@@ -155,20 +148,6 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
155
148
  }
156
149
  continue;
157
150
  }
158
- if (/^Delivery\b/.test(line)) {
159
- const m = line.match(/^Delivery `([^`]+)`: `([^`]+)` hosts `([^`]*)`$/);
160
- if (!m || byId.get(m[1])?.kind !== 'env') {
161
- errors.push(`line ${n}: invalid Delivery line`);
162
- continue;
163
- }
164
- if (policies.has(m[1])) errors.push(`line ${n}: duplicate delivery for ${m[1]}`);
165
- try {
166
- policies.set(m[1], deliveryPolicy(m[2], m[3] === '-' ? [] : m[3].split(',').map((h) => h.trim())));
167
- } catch (e) {
168
- errors.push(`line ${n}: ${e.message}`);
169
- }
170
- continue;
171
- }
172
151
  if (/^Selector:/.test(line)) errors.push(`line ${n}: legacy selectors are retired`);
173
152
  const d = line.match(/^<!-- plan ([0-9a-f]+):/);
174
153
  if (d) {
@@ -195,27 +174,21 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
195
174
  errors.push('different scan or org Secret state; scan, list Secrets and write a new plan');
196
175
  for (const it of scan.items) {
197
176
  if (!states.has(it.id)) errors.push(`\`${it.id}\` is missing from the plan`);
198
- if (it.kind === 'env' && !policies.has(it.id))
199
- errors.push(`\`${it.id}\` needs an explicit delivery mode and hosts line`);
200
177
  }
201
178
  if (errors.length) throw new PlanError(errors);
202
179
 
203
180
  const picked = scan.items.filter((it) => states.get(it.id));
204
- const ghToken = picked.some(
205
- (it) => it.kind === 'env' && it.name === 'GH_TOKEN' && policies.get(it.id).delivery === 'env',
206
- );
207
181
  const items = picked.map((it) => {
208
182
  const item = { ...it };
209
183
  delete item.checked;
210
184
  if (it.kind === 'env') {
211
- Object.assign(item, policies.get(it.id));
212
185
  item.expected = secrets.find((s) => s.name === it.name);
213
186
  item.expected = item.expected ? secretIdentity(item.expected) : null;
214
187
  } else {
215
188
  if (!sandboxes.length) errors.push(`\`${it.id}\`: config copying requires explicit running sandbox IDs`);
216
189
  item.files = it.files.map((f) => {
217
190
  if (!configPathOK(f.path)) errors.push(`\`${it.id}\`: invalid config path`);
218
- const c = fileContent(f, { home: scan.home, ghToken });
191
+ const c = fileContent(f, { home: scan.home });
219
192
  if (c.error) errors.push(`\`${it.id}\`: ${c.error}`);
220
193
  const bytes = c.error ? 0 : Buffer.byteLength(c.text);
221
194
  if (bytes > FILE_MAX_VALUE) errors.push(`\`${it.id}\`: file exceeds the config-copy limit of 64 KiB`);
@@ -249,11 +222,10 @@ export function parsePlan(scan, text, { secrets = [], manifest = { secrets: [] }
249
222
  if (fileBytes > FILE_MAX_TOTAL)
250
223
  throw new PlanError(['config copies exceed 512 KiB; uncheck files or transfer them separately'], 3);
251
224
  return {
252
- version: 3,
225
+ version: FORMAT,
253
226
  home: scan.home,
254
227
  orgId,
255
228
  sandboxes,
256
- ghToken,
257
229
  items,
258
230
  manual: scan.manual,
259
231
  skipped: scan.items.filter((it) => !states.get(it.id)).map((it) => it.id),
@@ -285,14 +257,11 @@ function main() {
285
257
  const secrets = JSON.parse(readFileSync(secretsFile, 'utf8')).items;
286
258
  if (!Array.isArray(secrets)) throw new Error('metadata must be instabox secrets ls --json output');
287
259
  if (command === 'write') {
288
- const sandboxes = [],
289
- hosts = [];
260
+ const sandboxes = [];
290
261
  let value;
291
262
  while ((value = opt('--sandbox')) !== undefined) sandboxes.push(value);
292
- while ((value = opt('--host')) !== undefined) hosts.push(value);
293
- const delivery = opt('--delivery') || 'env';
294
263
  if (args.length) throw new Error('unknown write arguments');
295
- process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes, delivery, hosts }));
264
+ process.stdout.write(writePlan(scan, { secrets, orgId, manifest, sandboxes }));
296
265
  } else if (command === 'parse') {
297
266
  const limitFile = opt('--limits');
298
267
  const limits = limitFile ? JSON.parse(readFileSync(limitFile, 'utf8')) : {};
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- // Save reviewed org Secrets, select them for named sandboxes, and copy ordinary
3
- // config files separately. Credential values are held only in memory/stdin.
2
+ // Save reviewed org Secrets for their registry hosts, select them for named sandboxes, and copy
3
+ // ordinary config files separately. Credential values are held only in memory/stdin.
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { randomBytes } from 'node:crypto';
6
6
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
@@ -13,7 +13,8 @@ import {
13
13
  FILE_MAX_VALUE,
14
14
  FILE_MAX_TOTAL,
15
15
  configPathOK,
16
- deliveryPolicy,
16
+ FORMAT,
17
+ TOKEN_HOSTS,
17
18
  human,
18
19
  isMain,
19
20
  run,
@@ -107,8 +108,8 @@ function validSource(it, home) {
107
108
  }
108
109
 
109
110
  function checkSelection(sel) {
110
- if (sel.version !== 3 || Object.hasOwn(sel, 'selector'))
111
- throw new Error('legacy selection format is retired; scan and approve a new org Secret plan');
111
+ if (sel.version !== FORMAT || Object.hasOwn(sel, 'selector'))
112
+ throw new Error('this selection was approved in an older format; scan and approve a new org Secret plan');
112
113
  if (
113
114
  !validID(sel.orgId) ||
114
115
  !Array.isArray(sel.sandboxes) ||
@@ -124,12 +125,15 @@ function checkSelection(sel) {
124
125
  if (ids.has(it.id)) throw new Error('duplicate selection ID');
125
126
  ids.add(it.id);
126
127
  if (it.kind === 'env') {
127
- if (!secretNameOK(it.name) || names.has(it.name) || !validSource(it, sel.home) || !Object.hasOwn(it, 'expected'))
128
+ if (
129
+ !TOKEN_HOSTS.has(it.name) ||
130
+ !secretNameOK(it.name) ||
131
+ names.has(it.name) ||
132
+ !validSource(it, sel.home) ||
133
+ !Object.hasOwn(it, 'expected')
134
+ )
128
135
  throw new Error('invalid Secret selection or source');
129
136
  names.add(it.name);
130
- const policy = deliveryPolicy(it.delivery, it.hosts);
131
- if (JSON.stringify(policy.hosts) !== JSON.stringify(it.hosts))
132
- throw new Error('hosts must use the normalized policy from the approved plan');
133
137
  } else if (it.kind === 'file') {
134
138
  if (!sel.sandboxes.length || !Array.isArray(it.files))
135
139
  throw new Error('config copying needs explicit sandbox IDs');
@@ -191,7 +195,7 @@ export function push(
191
195
  if (sel.items.some((it) => it.kind === 'file') && box.state !== 'running')
192
196
  throw new Error(`${id}: start the sandbox before copying config files`);
193
197
  const selection = json(['secrets', 'show', id]);
194
- if (!Array.isArray(selection.envSecrets) || (selection.secrets !== 'all' && !Array.isArray(selection.secrets)))
198
+ if (selection.secrets !== 'all' && !Array.isArray(selection.secrets))
195
199
  throw new Error('sandbox returned invalid Secret selections');
196
200
  return box;
197
201
  });
@@ -211,7 +215,6 @@ export function push(
211
215
 
212
216
  const tokens = [],
213
217
  files = [];
214
- const ghToken = sel.items.some((it) => it.kind === 'env' && it.name === 'GH_TOKEN' && it.delivery === 'env');
215
218
  const rcNames = new Map(),
216
219
  rcValues = new Map();
217
220
  for (const it of sel.items)
@@ -229,13 +232,13 @@ export function push(
229
232
  typeof value !== 'string' ||
230
233
  value.includes('\0') ||
231
234
  Buffer.byteLength(value) > SECRET_MAX_VALUE ||
232
- (it.delivery === 'proxy' && /[\x00-\x08\x0a-\x1f\x7f]/.test(value))
235
+ /[\x00-\x08\x0a-\x1f\x7f]/.test(value)
233
236
  )
234
- return fail(`${it.name}: could not prepare a valid ${it.delivery} value; nothing written`);
235
- tokens.push({ item: it, value });
237
+ return fail(`${it.name}: could not prepare a valid header value; nothing written`);
238
+ tokens.push({ item: it, hosts: TOKEN_HOSTS.get(it.name), value });
236
239
  } else {
237
240
  for (const f of it.files) {
238
- const got = fileContent(f, { home: sel.home, ghToken });
241
+ const got = fileContent(f, { home: sel.home });
239
242
  if (got.error || Buffer.byteLength(got.text) > FILE_MAX_VALUE)
240
243
  return fail(`~/${f.path}: config could not be prepared; nothing written`);
241
244
  files.push({ path: f.path, text: got.text });
@@ -267,11 +270,11 @@ export function push(
267
270
 
268
271
  const manifest = readManifest(manifestPath);
269
272
  let ok = true;
270
- for (const { item, value } of tokens) {
273
+ for (const { item, hosts, value } of tokens) {
271
274
  const r = {
272
275
  name: item.name,
273
276
  kind: 'secret',
274
- delivery: item.delivery,
277
+ hosts,
275
278
  bytes: Buffer.byteLength(value),
276
279
  status: dryRun ? 'would save' : 'failed',
277
280
  };
@@ -284,9 +287,7 @@ export function push(
284
287
  item.name,
285
288
  '--org',
286
289
  sel.orgId,
287
- '--delivery',
288
- item.delivery,
289
- ...item.hosts.flatMap((h) => ['--host', h]),
290
+ ...hosts.flatMap((h) => ['--host', h]),
290
291
  '--json',
291
292
  ];
292
293
  const response = run(args, { env, input: value + '\n' });
@@ -298,8 +299,7 @@ export function push(
298
299
  saved.orgId !== sel.orgId ||
299
300
  !saved.id ||
300
301
  !Number.isSafeInteger(saved.version) ||
301
- saved.delivery !== item.delivery ||
302
- JSON.stringify(deliveryPolicy(saved.delivery, saved.hosts || []).hosts) !== JSON.stringify(item.hosts)
302
+ JSON.stringify(unique(saved.hosts || [])) !== JSON.stringify(unique(hosts))
303
303
  )
304
304
  throw new Error();
305
305
  r.status = 'saved';
@@ -310,7 +310,7 @@ export function push(
310
310
  }
311
311
  }
312
312
  log(
313
- `${r.status} ${item.name} (${item.delivery}${item.hosts.length ? ', ' + item.hosts.join(',') : ''}, ${r.bytes} bytes) in ${sel.orgId}`,
313
+ `${r.status} ${item.name} (for ${hosts.join(',')}, ${r.bytes} bytes) in ${sel.orgId}`,
314
314
  );
315
315
  }
316
316
  if (!dryRun && tokens.length) saveManifest(manifestPath, manifest);
@@ -333,17 +333,14 @@ export function push(
333
333
  try {
334
334
  if (tokens.length) {
335
335
  const current = json(['secrets', 'show', box.id]);
336
- const envNames = tokens.filter((t) => t.item.delivery === 'env').map((t) => t.item.name);
337
- const proxyNames = tokens.filter((t) => t.item.delivery === 'proxy').map((t) => t.item.name);
338
- const flags = [];
339
- if (envNames.length) flags.push('--env-secrets', JSON.stringify(unique([...current.envSecrets, ...envNames])));
340
- if (proxyNames.length && current.secrets !== 'all')
341
- flags.push('--secrets', JSON.stringify(unique([...current.secrets, ...proxyNames])));
342
- if (flags.length) json(['secrets', 'select', box.id, ...flags]);
336
+ if (current.secrets !== 'all') {
337
+ const names = unique([...current.secrets, ...tokens.map((t) => t.item.name)]);
338
+ json(['secrets', 'select', box.id, '--secrets', JSON.stringify(names)]);
339
+ }
343
340
  if (box.state === 'running') {
344
341
  const refreshed = json(['secrets', 'push', box.id]);
345
342
  if (refreshed.guest?.state !== 'applied')
346
- throw new Error('guest delivery is pending or failed; inspect secrets show and retry');
343
+ throw new Error('the box has not applied the Secret selection; inspect secrets show and retry');
347
344
  }
348
345
  }
349
346
  if (files.length) {
@@ -377,7 +374,7 @@ export function push(
377
374
  }
378
375
  result.status = 'configured';
379
376
  log(
380
- `configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; Secret delivery waits for the next start'}`,
377
+ `configured ${box.id}${box.state === 'running' ? '; existing application processes may need a restart' : '; the Secret selection applies at the next start'}`,
381
378
  );
382
379
  } catch (e) {
383
380
  ok = false;
@@ -13,7 +13,8 @@ import {
13
13
  MACOS_ONLY,
14
14
  SECRET_VALUE,
15
15
  FILE_MAX_VALUE,
16
- WELL_KNOWN,
16
+ FORMAT,
17
+ TOKEN_HOSTS,
17
18
  hasCommand,
18
19
  isMain,
19
20
  isSecretName,
@@ -76,7 +77,6 @@ function scanTokens({ home, env, procEnv }) {
76
77
  byName.set(name, item);
77
78
  items.push(item);
78
79
  };
79
- const reserved = new Set();
80
80
 
81
81
  // gh: the token gh keeps (keychain or hosts.yml), read with `gh auth token` at push time.
82
82
  if (hasCommand('gh', env)) {
@@ -84,42 +84,54 @@ function scanTokens({ home, env, procEnv }) {
84
84
  if (st.ok) {
85
85
  const who = (st.stdout + st.stderr).match(/account (\S+)/)?.[1] || null;
86
86
  add('GH_TOKEN', { type: 'cmd', argv: ['gh', 'auth', 'token', '--hostname', 'github.com'] }, '`gh auth token`', {
87
- note: `${who ? `GitHub account ${who}; ` : ''}gh, and git over HTTPS to GitHub, use it on the box`,
87
+ note: `${who ? `GitHub account ${who}; ` : ''}gh uses it on the box; git over HTTPS needs its own sign-in`,
88
88
  });
89
89
  }
90
90
  }
91
91
 
92
- // Shell startup files: exported tokens.
92
+ // Shell startup files: exported tokens. Only names in TOKEN_HOSTS can be saved; any other
93
+ // credential-looking export is a manual step.
94
+ const listed = new Set();
95
+ const listManual = (name, place, why, todo) => {
96
+ if (listed.has(name)) return;
97
+ listed.add(name);
98
+ manual.push({ what: `${name} (${place})`, why, todo });
99
+ };
93
100
  for (const f of SHELL_RC) {
94
101
  const text = readText(join(home, f));
95
102
  if (text === null) continue;
96
103
  for (const a of shellAssignments(text)) {
97
- if (!WELL_KNOWN.has(a.name) && !isSecretName(a.name) && !a.secretValue) continue;
104
+ if (!TOKEN_HOSTS.has(a.name) && !isSecretName(a.name) && !a.secretValue) continue;
105
+ const place = `~/${f}:${a.line}`;
98
106
  if (!secretNameOK(a.name)) {
99
- if (!reserved.has(a.name)) {
100
- reserved.add(a.name);
101
- manual.push({
102
- what: `${a.name} (~/${f}:${a.line})`,
103
- why: 'Secrets reserve this variable name for sandbox or trust configuration',
104
- todo: `if a box needs it, set it there by hand`,
105
- });
106
- }
107
+ listManual(
108
+ a.name,
109
+ place,
110
+ 'Secrets reserve this variable name for sandbox or trust configuration',
111
+ 'if a box needs it, set it there by hand',
112
+ );
113
+ continue;
114
+ }
115
+ if (!TOKEN_HOSTS.has(a.name)) {
116
+ listManual(
117
+ a.name,
118
+ place,
119
+ 'this skill does not know which HTTPS API hosts it is for',
120
+ `if its client sends it in an HTTPS header, \`instabox secrets set ${a.name} --org <org> --host <api-host>\` and select it on the box; other credentials need their own setup there`,
121
+ );
107
122
  continue;
108
123
  }
109
124
  const extra = {};
110
125
  if (a.keychain) {
111
126
  extra.checked = false;
112
127
  extra.reason = 'your shell reads it from the macOS keychain: checking it runs that lookup once, at push time';
113
- } else if (!WELL_KNOWN.has(a.name)) {
114
- extra.checked = false;
115
- extra.reason = 'not a well-known model or service token: check it only for the org and sandboxes in the plan';
116
128
  }
117
- add(a.name, { type: 'rc', file: join(home, f), name: a.name }, `~/${f}:${a.line}`, extra);
129
+ add(a.name, { type: 'rc', file: join(home, f), name: a.name }, place, extra);
118
130
  }
119
131
  }
120
- // Well-known tokens in the environment the scan runs in (set some other way than a shell file).
132
+ // Known tokens in the environment the scan runs in (set some other way than a shell file).
121
133
  for (const name of Object.keys(procEnv).sort()) {
122
- if (!WELL_KNOWN.has(name) || !procEnv[name]) continue;
134
+ if (!TOKEN_HOSTS.has(name) || !procEnv[name]) continue;
123
135
  add(name, { type: 'env', name }, 'this shell’s environment');
124
136
  }
125
137
 
@@ -127,23 +139,20 @@ function scanTokens({ home, env, procEnv }) {
127
139
  manual.push({
128
140
  what: 'GitHub token (GH_TOKEN)',
129
141
  why: hasCommand('gh', env) ? 'gh is not signed in on this machine' : 'gh is not installed on this machine',
130
- 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> --delivery env` (it prompts without echo)',
131
- });
132
- if (!byName.has('CLAUDE_CODE_OAUTH_TOKEN'))
133
- manual.push({
134
- what: 'Claude Code sign-in (CLAUDE_CODE_OAUTH_TOKEN)',
135
- why: 'not found in your shell files; the sign-in on this machine is in the macOS keychain, and is never read',
136
- todo:
137
- 'run `claude setup-token` yourself, then `instabox secrets set CLAUDE_CODE_OAUTH_TOKEN --org <org> --delivery env` and paste it at the prompt' +
138
- (byName.has('ANTHROPIC_API_KEY')
139
- ? ' (or skip it: ANTHROPIC_API_KEY is in the plan, and Claude Code uses that)'
140
- : '; or `claude`, then /login, on each box'),
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)',
141
143
  });
144
+ manual.push({
145
+ what: 'Claude Code sign-in',
146
+ why: 'the sign-in on this machine is in the macOS keychain, and is never read',
147
+ todo: byName.has('ANTHROPIC_API_KEY')
148
+ ? 'skip it: ANTHROPIC_API_KEY is in the plan, and Claude Code uses that; or run `claude`, then /login, on each box'
149
+ : 'run `claude`, then /login, on each box',
150
+ });
142
151
  if (!byName.has('OPENAI_API_KEY') && (hasCommand('codex', env) || statOrNull(join(home, '.codex'))))
143
152
  manual.push({
144
153
  what: 'Codex sign-in',
145
154
  why: 'no OPENAI_API_KEY in your shell files; a ChatGPT sign-in cannot be copied (its refresh token rotates)',
146
- todo: 'on each box: `codex login --device-auth`; or `instabox secrets set OPENAI_API_KEY --org <org> --delivery env`, then select it explicitly for each intended sandbox',
155
+ todo: 'on each box: `codex login --device-auth`; or `instabox secrets set OPENAI_API_KEY --org <org> --host api.openai.com`, then select it on each box',
147
156
  });
148
157
  return { items, manual };
149
158
  }
@@ -259,7 +268,7 @@ export function scan(opts = {}) {
259
268
  const home = resolve(opts.home || env.HOME);
260
269
  const tokens = scanTokens({ home, env, procEnv: opts.procEnv || env });
261
270
  const items = [...tokens.items, ...scanGit({ home, env }), ...scanEditor({ home })];
262
- return { version: 3, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
271
+ return { version: FORMAT, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
263
272
  }
264
273
 
265
274
  function main() {
@@ -20,16 +20,16 @@ Three scripts in `scripts/` next to this file do the parts that must be exact (N
20
20
  What goes over:
21
21
 
22
22
  - All of the main checkout's `.git` (branches, tags, stashes, unpushed commits, the index so staged changes stay staged, each worktree's own HEAD and index), every tracked file, and every untracked file git does not ignore, in the main checkout and in each linked worktree.
23
- - **Never build output or dependencies**, even when the repo forgot to ignore them: `node_modules`, `target`, `dist`, `build`, `out`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.cache`, `coverage`, `__pycache__`, `.venv`, `venv`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `bazel-*`, `*.egg-info`, `*.o`, `*.so`, `*.pyc`, `*.class`, ... (the full list is `EXCLUDED_DIRS` and `EXCLUDED_FILES` in `repo-files.mjs`). Files git ignores stay too. A **tracked** file always goes, even under such a name, so `git status` matches. `--keep NAME` copies an excluded name anyway, for a repo whose source really lives in, say, `build/` (pass it to every `repo-files.mjs` run, step 1 and step 3, through `KEEP`). It does not bring back an untracked `.env`: their values need explicit Secret import and selection (step 7).
23
+ - **Never build output or dependencies**, even when the repo forgot to ignore them: `node_modules`, `target`, `dist`, `build`, `out`, `.next`, `.nuxt`, `.svelte-kit`, `.turbo`, `.cache`, `coverage`, `__pycache__`, `.venv`, `venv`, `.tox`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `bazel-*`, `*.egg-info`, `*.o`, `*.so`, `*.pyc`, `*.class`, ... (the full list is `EXCLUDED_DIRS` and `EXCLUDED_FILES` in `repo-files.mjs`). Files git ignores stay too. A **tracked** file always goes, even under such a name, so `git status` matches. `--keep NAME` copies an excluded name anyway, for a repo whose source really lives in, say, `build/` (pass it to every `repo-files.mjs` run, step 1 and step 3, through `KEEP`). It does not bring back an untracked `.env`: those stay behind (step 7 lists them).
24
24
  - Claude Code and Codex conversations for the repo and its worktrees (on by default, the user can opt out).
25
25
 
26
- Not moved: `.env` files that are not committed (their values can be imported as Secrets in step 7), credentials stored outside the repo (git credential helpers, gh, cloud, API keys in your environment), other local tools and config, other repos. A `.env` that is committed or staged is in `.git` and goes with it. Everything inside `.git` goes, including `.git/config` and so a remote URL with a token embedded in it (`https://x-access-token:TOKEN@...`); and only `.env`-named untracked files are held back, so an untracked `.npmrc` with a token, a `*.pem` or a `secrets.yaml` that git does not ignore goes too.
26
+ Not moved: `.env` files that are not committed, credentials stored outside the repo (git credential helpers, gh, cloud, API keys in your environment), other local tools and config, other repos. A `.env` that is committed or staged is in `.git` and goes with it. Everything inside `.git` goes, including `.git/config` and so a remote URL with a token embedded in it (`https://x-access-token:TOKEN@...`); and only `.env`-named untracked files are held back, so an untracked `.npmrc` with a token, a `*.pem` or a `secrets.yaml` that git does not ignore goes too.
27
27
 
28
28
  **Never read, print or paste a credential value yourself**: not a `.env`, not `gh auth token`, not `echo $SOME_KEY`.
29
29
 
30
30
  Before starting, tell the user plainly:
31
31
 
32
- - **git push/pull from the box will not work until they set up credentials there** (for example `instabox ssh <box>` then `gh auth login`; org Secrets support key=value credentials, while git configuration must be set up separately).
32
+ - **git push/pull from the box will not work until they set up credentials there** (for example `instabox ssh <box>` then `gh auth login`; org Secrets cover HTTP API keys only, so git sign-in is set up separately).
33
33
  - **The copied conversation history may contain secrets they pasted earlier** (tokens, keys, passwords). It goes by default; ask whether to skip it (or only Codex / only Claude Code).
34
34
  - Their local copy stays as it is, and nothing keeps the two in sync afterwards.
35
35
 
@@ -72,7 +72,7 @@ BOX=<box>
72
72
  instabox create --name "$BOX" --meta repo="$(basename "$DEST")"
73
73
  ```
74
74
 
75
- Box names are lowercase letters, digits and `-`. The box gets the default size, 4 vCPU / 8 GB (the instabox skill's Sizes lists the others a plan offers). The default template (`agent-browser`, shown as standard in the dashboard) has git, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker and a desktop with Chromium. This box is meant to stay; it pauses itself after 30 idle minutes. The `repo=` label can match existing Vault selectors; it does not select org Secrets. Without Secret selection flags the box keeps the existing Vault behavior. Opt into org Secrets explicitly with `--secrets` or `--env-secrets`; at creation, `--env-secrets` alone also selects all org proxy Secrets. Use `--no-vault --secrets none --env-secrets none` when neither store should deliver credentials.
75
+ Box names are lowercase letters, digits and `-`. The box gets the default size, 4 vCPU / 8 GB (the instabox skill's Sizes lists the others a plan offers). The default template (`agent-browser`, shown as standard in the dashboard) has git, Node.js 22, Python (uv), Go, Rust, a C toolchain, Docker and a desktop with Chromium. This box is meant to stay; it pauses itself after 30 idle minutes. The `repo=` label can match a user's existing Vault selectors (the Vault is legacy and will be retired; never save credentials there); it does not select org Secrets. Without `--secrets` the box still gets such existing Vault items. Opt into org Secrets explicitly with `--secrets NAME,NAME`. Use `--no-vault --secrets none` when neither store should deliver credentials.
76
76
 
77
77
  ## 3. Copy the repo and its worktrees
78
78
 
@@ -111,7 +111,7 @@ done
111
111
  git worktree list; instabox exec "$BOX" --cwd "$DEST" -- git worktree list
112
112
  ```
113
113
 
114
- The only allowed differences are `<` lines starting with `??` for what the plan left out (a build directory, an untracked `.env`, or a whole `?? dir/` that held only those), and ` M <submodule>` for a submodule with uncommitted changes inside it (those are not copied: note it for step 8). A name passed with `--keep` must not show up as a `??` difference: it was meant to go. HEADs must match, and the box's `git worktree list` must show the same branches, detached HEADs and `locked` marks at the box paths, without the worktrees left behind. Anything else: stop and look before going on.
114
+ The only allowed differences are `<` lines starting with `??` for what the plan left out (a build directory, an untracked `.env`, or a whole `?? dir/` that held only those), and ` M <submodule>` for a submodule with uncommitted changes inside it (those are not copied: note it for step 7). A name passed with `--keep` must not show up as a `??` difference: it was meant to go. HEADs must match, and the box's `git worktree list` must show the same branches, detached HEADs and `locked` marks at the box paths, without the worktrees left behind. Anything else: stop and look before going on.
115
115
 
116
116
  ## 4. Conversation history (unless the user opted out)
117
117
 
@@ -165,36 +165,13 @@ instabox exec "$BOX" --cwd "$DEST" -- 'git status --porcelain | sort; git rev-pa
165
165
 
166
166
  Compare with step 3. New `??` lines from the installs (for example a lockfile the project does not commit) are worth a mention; a modified tracked file is not expected: find which step did it.
167
167
 
168
- ## 7. `.env` values: explicit Secret import and selection
169
-
170
- The plan's `envLeftOut` lists excluded files by name (`jq -r '.main.envLeftOut, (.worktrees[] | select(.move) | .envLeftOut) | .[]' "$W/plan.json"`). Do not print their contents. The new Secret model stores individual key=value entries, not files, and does not create or link a `.env` in the repo.
171
-
172
- For a project that needs the real values, explain that env delivery makes them readable by code in the box. Use the user's authorization to import the intended file and select its names for this box. Import is org-wide and replaces existing entries with the same names: resolve conflicts and confirm any intended replacement before importing. Different projects needing different values under the same variable name should use separate orgs, or configure their applications to use distinct names; do not silently rename or overwrite a shared credential.
173
-
174
- After that choice, per approved file (the CLI reads values; saved JSON contains metadata only):
175
-
176
- ```bash
177
- BOX_ORG=$(instabox get "$BOX" --json | jq -r '.orgId')
178
- instabox secrets show "$BOX" --json > "$W/selected-secrets.json"
179
- instabox secrets import "<local .env path>" --org "$BOX_ORG" --delivery env --json > "$W/imported-secrets.json"
180
- ENV_NAMES=$(jq -c -s '.[0].envSecrets + [.[1].items[].name] | unique' "$W/selected-secrets.json" "$W/imported-secrets.json")
181
- instabox secrets select "$BOX" --env-secrets "$ENV_NAMES"
182
- instabox secrets push "$BOX"
183
- ```
184
-
185
- This merges the approved env names with the box's current env selection and leaves its proxy selection unchanged. Import alone grants no env access to any box. Start a new login shell, or restart/reconfigure the application, to load the updated environment. Use silent presence checks; do not print values to verify delivery.
186
-
187
- For credentials sent literally in HTTPS headers, save them with `--delivery proxy --host <approved-host>` instead, and select the corresponding proxy names. Every entry in one import shares its delivery mode and host scope, so do not import a mixed `.env` as proxy credentials without separating those decisions. A native database client or a client that computes signatures needs an explicitly selected env value.
188
-
189
- If the application requires a physical `.env` or another credential file, it needs a separate, explicitly authorized file-transfer or guest-generation workflow with restricted file permissions. Such a file is readable in the guest and may be included in snapshots; it has no Secret-managed file cleanup. Report this requirement rather than claiming Secret import recreates the original file. Tokens and local editor or git configuration are separate setup decisions; metadata labels do not grant credentials.
190
-
191
- ## 8. Tell the user
168
+ ## 7. Tell the user
192
169
 
193
170
  - The box name and where the repo is: `instabox ssh <box>`, then `cd /data/workspace/<name>`; each worktree with its box path and branch; worktrees left behind and why.
194
171
  - Uncommitted work inside a submodule (a ` M <submodule>` line in step 3) did not come over: name each such submodule so the user can commit or copy that work themselves.
195
172
  - Sizes: on disk before, sent after, and the biggest things left out (they were rebuilt or reinstalled on the box).
196
173
  - What was installed, whether build and tests pass, and anything left to do (each FAILED step and note from `deps.sh` that is not fixed).
197
- - Again: git push/pull needs credentials set up on the box. State which Secret names and delivery modes were selected, if any; never include values. Ignored files, build output and untracked `.env` files were not copied. Secrets in committed or staged files, anywhere in `.git` (such as a token in a remote URL in `.git/config`), in untracked files not named `.env*`, or in the copied history, did go over.
174
+ - Again: git push/pull needs credentials set up on the box. Ignored files and build output were not copied. Untracked `.env` files were left behind: list them by name from the plan (`jq -r '.main.envLeftOut, (.worktrees[] | select(.move) | .envLeftOut) | .[]' "$W/plan.json"`), never their contents. For an HTTP API key the project needs, the user can run `instabox secrets set NAME --host <api-host>` and select it on the box with `instabox secrets select`; database URLs and other credentials need their own setup on the box. Secrets in committed or staged files, anywhere in `.git` (such as a token in a remote URL in `.git/config`), in untracked files not named `.env*`, or in the copied history, did go over.
198
175
  - How to pick up old conversations, kept short, with the session ids from step 4:
199
176
  - Claude Code, in the repo (or worktree) directory on the box: `claude --continue` (latest) or `claude --resume` (picker, or `claude --resume <id>`).
200
177
  - Codex, in that directory: `codex resume` (picker for this directory), `codex resume --last`, or `codex resume <id>`.
@@ -325,7 +325,7 @@ project() { # dir label
325
325
  note "$L: has a compose file; its services were not started (docker compose up -d)"
326
326
  for f in .env.example .env.sample .env.template; do
327
327
  if [ -f "$f" ] && [ ! -e .env ]; then
328
- note "$L: expects a .env (see $f); .env files are not copied: bring it through the Vault (instabox-move-repo skill, .env files)"
328
+ note "$L: expects a .env (see $f); .env files are not copied: the user sets it up (instabox-move-repo skill, step 7)"
329
329
  break
330
330
  fi
331
331
  done
@@ -10,7 +10,7 @@
10
10
  // relative to PATH
11
11
  // --skip-worktree PATH leave a linked worktree behind (repeatable; pass it to --list0 runs too)
12
12
  // --keep NAME copy an excluded name anyway (repeatable): --keep build (not an untracked
13
- // .env: those go through the Vault)
13
+ // .env: those always stay behind)
14
14
  //
15
15
  // What goes: all of the main checkout's .git (branches, stashes, the index, unpushed commits, the
16
16
  // worktrees' own HEAD and index under .git/worktrees/), every tracked file that exists, and every
@@ -108,7 +108,7 @@ export const EXCLUDED_FILES = [
108
108
  '.DS_Store',
109
109
  ];
110
110
 
111
- // Untracked .env files never go (secrets belong in the Vault). Templates do.
111
+ // Untracked .env files never go: the final report lists them as left behind. Templates do.
112
112
  const ENV_FILE = /^\.env(?:\..+)?$/;
113
113
  const ENV_TEMPLATE = /^\.env\.(?:example|sample|template|dist|defaults)$/;
114
114
  const isEnvName = (f) => {
@@ -18,7 +18,7 @@ instabox snapshot save work --name work-ready --description "deps installed, rep
18
18
  - A running box is saved live, without stopping it: the snapshot is `pending` in `instabox snapshot ls` until its disks finish uploading (`--no-wait` returns at once).
19
19
  - Do not stop the box first: `stop` ends every process in it. A live snapshot is like a clean power cut: what is on disk is kept, what programs hold only in memory is not. Only if the server answers that this box must be stopped, or if a program must write its data to disk first (a database mid-write), run `instabox stop work`, save, then `instabox start work`.
20
20
  - `--root-only` saves just the root disk (smaller; `/data` is not included).
21
- - A disk snapshot includes credential files written by applications, tool sign-ins, and historical Vault files. The managed Secret environment lives on tmpfs, but applications can copy real env values to disk; a suspend snapshot can also retain them in process memory. Do not describe snapshots as credential-free. Review copied credentials and rotate upstream when required before sharing or retaining them.
21
+ - A disk snapshot includes credential files written by applications, tool sign-ins, and historical Vault files; a suspend snapshot can also retain credentials in process memory. Org Secrets reach the box only as placeholders, so a snapshot holds none of their values, but anything an app or a person wrote to disk is kept. Do not describe snapshots as credential-free. Review copied credentials and rotate upstream when required before sharing or retaining them.
22
22
  - Pick names that say what is inside (`node20-playwright`, `api-db-seeded`); names must be unique among your snapshots.
23
23
 
24
24
  ## Load
@@ -50,13 +50,13 @@ instabox create --name work2 --snapshot work-copy
50
50
  instabox snapshot rm work-copy # optional; work2 is not affected
51
51
  ```
52
52
 
53
- The copy gets a new sandbox identity and the selections in its creation request. Without Secret selection flags it keeps the existing Vault behavior and does not enroll in org Secrets; it does not inherit the source's explicit grants or exclusions. To disable new delivery from both stores, use:
53
+ 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:
54
54
 
55
55
  ```sh
56
- instabox create --name work2 --snapshot work-copy --no-vault --secrets none --env-secrets none
56
+ instabox create --name work2 --snapshot work-copy --no-vault --secrets none
57
57
  ```
58
58
 
59
- To provide a database credential, select its env Secret explicitly with `--env-secrets DATABASE_URL`. At creation that flag alone also selects all org proxy Secrets; add `--secrets none` or specific names to narrow proxy access. Copied placeholders cannot authorize use as the source sandbox. New selections do not erase real credentials already present in copied files; a new proxy identity does not make those files safe to share.
59
+ To give the copy API credentials, name them with `--secrets NAME,NAME`. Placeholders copied from the source box do not work for the new one. New selections do not erase credentials that apps or people already wrote into files on the copied disks; review those before sharing the snapshot or the box.
60
60
 
61
61
  ## Clean up
62
62