instabox 0.6.0 → 0.6.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Read-only inventory of the basic setup that can go into the instabox Vault:
2
+ // Read-only inventory of the basic setup for org Secrets and explicit sandbox config copies:
3
3
  // tokens (by name and place, never values), git config, editor and terminal
4
4
  // config. Prints JSON.
5
5
  //
@@ -12,7 +12,7 @@ import { join, relative, resolve } from 'node:path';
12
12
  import {
13
13
  MACOS_ONLY,
14
14
  SECRET_VALUE,
15
- VAULT_MAX_VALUE,
15
+ FILE_MAX_VALUE,
16
16
  WELL_KNOWN,
17
17
  hasCommand,
18
18
  isMain,
@@ -21,16 +21,37 @@ import {
21
21
  readText,
22
22
  run,
23
23
  statOrNull,
24
- vaultEnvNameOK,
25
- vaultFileName,
26
- vaultPathOK,
24
+ secretNameOK,
25
+ configPathOK,
27
26
  } from './lib.mjs';
28
27
 
29
28
  // Shell startup files: read for exported tokens only. They do not move.
30
- export const SHELL_RC = ['.zshenv', '.zprofile', '.zshrc', '.bash_profile', '.bash_login', '.profile', '.bashrc', '.bash_aliases'];
29
+ export const SHELL_RC = [
30
+ '.zshenv',
31
+ '.zprofile',
32
+ '.zshrc',
33
+ '.bash_profile',
34
+ '.bash_login',
35
+ '.profile',
36
+ '.bashrc',
37
+ '.bash_aliases',
38
+ ];
31
39
 
32
40
  // nvim: config files only. Plugin managers' checkouts and editor state stay behind.
33
- const NVIM_SKIP = new Set(['.git', 'node_modules', 'pack', 'plugged', 'bundle', 'site', 'undo', 'backup', 'swap', 'sessions', 'spell', '.repro']);
41
+ const NVIM_SKIP = new Set([
42
+ '.git',
43
+ 'node_modules',
44
+ 'pack',
45
+ 'plugged',
46
+ 'bundle',
47
+ 'site',
48
+ 'undo',
49
+ 'backup',
50
+ 'swap',
51
+ 'sessions',
52
+ 'spell',
53
+ '.repro',
54
+ ]);
34
55
  const NVIM_MAX_FILES = 40;
35
56
  const NVIM_MAX_BYTES = 128 * 1024;
36
57
 
@@ -50,12 +71,12 @@ function scanTokens({ home, env, procEnv }) {
50
71
  const manual = [];
51
72
  const byName = new Map();
52
73
  const add = (name, source, from, extra = {}) => {
53
- if (byName.has(name)) return; // the first place a name is found wins: the Vault holds one value per name
74
+ if (byName.has(name)) return; // the first place a name is found wins: an org holds one value per Secret name
54
75
  const item = { id: `env:${name}`, section: 'tokens', kind: 'env', name, source, from, checked: true, ...extra };
55
76
  byName.set(name, item);
56
77
  items.push(item);
57
78
  };
58
- const unvaultable = new Set();
79
+ const reserved = new Set();
59
80
 
60
81
  // gh: the token gh keeps (keychain or hosts.yml), read with `gh auth token` at push time.
61
82
  if (hasCommand('gh', env)) {
@@ -74,12 +95,12 @@ function scanTokens({ home, env, procEnv }) {
74
95
  if (text === null) continue;
75
96
  for (const a of shellAssignments(text)) {
76
97
  if (!WELL_KNOWN.has(a.name) && !isSecretName(a.name) && !a.secretValue) continue;
77
- if (!vaultEnvNameOK(a.name)) {
78
- if (!unvaultable.has(a.name)) {
79
- unvaultable.add(a.name);
98
+ if (!secretNameOK(a.name)) {
99
+ if (!reserved.has(a.name)) {
100
+ reserved.add(a.name);
80
101
  manual.push({
81
102
  what: `${a.name} (~/${f}:${a.line})`,
82
- why: 'the Vault does not take env items with this name (INSTABOX_*, SSH_* and a few system names)',
103
+ why: 'Secrets reserve this variable name for sandbox or trust configuration',
83
104
  todo: `if a box needs it, set it there by hand`,
84
105
  });
85
106
  }
@@ -91,7 +112,7 @@ function scanTokens({ home, env, procEnv }) {
91
112
  extra.reason = 'your shell reads it from the macOS keychain: checking it runs that lookup once, at push time';
92
113
  } else if (!WELL_KNOWN.has(a.name)) {
93
114
  extra.checked = false;
94
- extra.reason = 'not a well-known model or service token: check it if every box should have it';
115
+ extra.reason = 'not a well-known model or service token: check it only for the org and sandboxes in the plan';
95
116
  }
96
117
  add(a.name, { type: 'rc', file: join(home, f), name: a.name }, `~/${f}:${a.line}`, extra);
97
118
  }
@@ -106,40 +127,51 @@ function scanTokens({ home, env, procEnv }) {
106
127
  manual.push({
107
128
  what: 'GitHub token (GH_TOKEN)',
108
129
  why: hasCommand('gh', env) ? 'gh is not signed in on this machine' : 'gh is not installed on this machine',
109
- todo: 'sign in here with `gh auth login` and run this again; or put a token in yourself: `instabox vault set GH_TOKEN` (it prompts without echo)',
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)',
110
131
  });
111
132
  if (!byName.has('CLAUDE_CODE_OAUTH_TOKEN'))
112
133
  manual.push({
113
134
  what: 'Claude Code sign-in (CLAUDE_CODE_OAUTH_TOKEN)',
114
135
  why: 'not found in your shell files; the sign-in on this machine is in the macOS keychain, and is never read',
115
136
  todo:
116
- 'run `claude setup-token` yourself (it prints a token that lasts a year), then `instabox vault set CLAUDE_CODE_OAUTH_TOKEN` and paste it at the prompt' +
117
- (byName.has('ANTHROPIC_API_KEY') ? ' (or skip it: ANTHROPIC_API_KEY is in the plan, and Claude Code uses that)' : '; or `claude`, then /login, on each box'),
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'),
118
141
  });
119
142
  if (!byName.has('OPENAI_API_KEY') && (hasCommand('codex', env) || statOrNull(join(home, '.codex'))))
120
143
  manual.push({
121
144
  what: 'Codex sign-in',
122
145
  why: 'no OPENAI_API_KEY in your shell files; a ChatGPT sign-in cannot be copied (its refresh token rotates)',
123
- todo: 'on each box: `codex login --device-auth`; or `instabox vault set OPENAI_API_KEY` once for every box',
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',
124
147
  });
125
148
  return { items, manual };
126
149
  }
127
150
 
128
- // A home-relative path for a file, or null when it is outside HOME or not one the Vault takes.
151
+ // A home-relative path for a config copy, or null for an unsafe target.
129
152
  function homePath(home, p) {
130
153
  const rel = relative(home, p);
131
- return rel && !rel.startsWith('..') && vaultPathOK(rel) ? rel : null;
154
+ return rel && !rel.startsWith('..') && configPathOK(rel) ? rel : null;
132
155
  }
133
156
 
134
157
  function fileEntry(home, section, rel, transform, extra = {}) {
135
158
  const src = join(home, rel);
136
159
  const st = statOrNull(src);
137
160
  if (!st?.isFile()) return null;
138
- const file = { path: rel, name: vaultFileName(rel), src, transform, bytes: st.size };
139
- const e = { id: `file:${rel}`, section, kind: 'file', path: rel, files: [file], bytes: st.size, checked: true, ...extra };
140
- if (st.size > VAULT_MAX_VALUE) {
161
+ const file = { path: rel, src, transform, bytes: st.size };
162
+ const e = {
163
+ id: `file:${rel}`,
164
+ section,
165
+ kind: 'file',
166
+ path: rel,
167
+ files: [file],
168
+ bytes: st.size,
169
+ checked: true,
170
+ ...extra,
171
+ };
172
+ if (st.size > FILE_MAX_VALUE) {
141
173
  e.checked = false;
142
- e.reason = `${st.size} bytes: over the Vault's 64 KiB per item`;
174
+ e.reason = `${st.size} bytes: over the config-copy limit of 64 KiB per file`;
143
175
  }
144
176
  return e;
145
177
  }
@@ -150,7 +182,9 @@ function scanGit({ home, env }) {
150
182
  note: 'osxkeychain and other macOS-only lines, credentials and commit signing left out',
151
183
  });
152
184
  if (gc) out.push(gc);
153
- const xdg = fileEntry(home, 'git', '.config/git/config', 'gitconfig', { note: 'macOS-only lines and credentials left out' });
185
+ const xdg = fileEntry(home, 'git', '.config/git/config', 'gitconfig', {
186
+ note: 'macOS-only lines and credentials left out',
187
+ });
154
188
  if (xdg) out.push(xdg);
155
189
  // The global gitignore: core.excludesfile, else git's default.
156
190
  let ignore = null;
@@ -189,22 +223,30 @@ function scanEditor({ home }) {
189
223
  if (!keep) continue;
190
224
  const rel = `.config/nvim/${f}`;
191
225
  const st = statOrNull(join(real, f));
192
- if (!vaultPathOK(rel) || !st) continue;
193
- if (st.size > VAULT_MAX_VALUE) {
226
+ if (!configPathOK(rel) || !st) continue;
227
+ if (st.size > FILE_MAX_VALUE) {
194
228
  left.push(`${f} (${st.size} bytes)`);
195
229
  continue;
196
230
  }
197
- files.push({ path: rel, name: vaultFileName(rel), src: join(real, f), transform: f.endsWith('.json') ? 'raw' : 'text', bytes: st.size });
231
+ files.push({ path: rel, src: join(real, f), transform: f.endsWith('.json') ? 'raw' : 'text', bytes: st.size });
198
232
  }
199
233
  if (files.some((f) => /^\.config\/nvim\/init\.(?:lua|vim)$/.test(f.path))) {
200
234
  const bytes = files.reduce((s, f) => s + f.bytes, 0);
201
- const e = { id: 'files:.config/nvim', section: 'editor', kind: 'file', path: '.config/nvim', files, bytes, checked: true };
235
+ const e = {
236
+ id: 'files:.config/nvim',
237
+ section: 'editor',
238
+ kind: 'file',
239
+ path: '.config/nvim',
240
+ files,
241
+ bytes,
242
+ checked: true,
243
+ };
202
244
  const notes = ['plugins are not copied: your plugin manager installs them on the box'];
203
245
  if (left.length) notes.push(`left out, over 64 KiB: ${left.join(', ')}`);
204
246
  e.note = notes.join('; ');
205
247
  if (files.length > NVIM_MAX_FILES || bytes > NVIM_MAX_BYTES) {
206
248
  e.checked = false;
207
- e.reason = `${files.length} files: a big share of the Vault's 100 items and 256 KiB`;
249
+ e.reason = `${files.length} files: over the suggested config-copy size; review before selecting`;
208
250
  }
209
251
  out.push(e);
210
252
  }
@@ -212,27 +254,12 @@ function scanEditor({ home }) {
212
254
  return out;
213
255
  }
214
256
 
215
- // Two paths can map to one item name (a-b.lua and a/b.lua): the second gets -2, -3...
216
- function uniqueNames(entries) {
217
- const used = new Set();
218
- for (const e of entries) {
219
- if (e.kind === 'env') used.add(e.name);
220
- for (const f of e.files || []) {
221
- let n = f.name;
222
- for (let i = 2; used.has(n); i++) n = `${f.name}-${i}`;
223
- f.name = n;
224
- used.add(n);
225
- }
226
- }
227
- }
228
-
229
257
  export function scan(opts = {}) {
230
258
  const env = opts.env || process.env;
231
259
  const home = resolve(opts.home || env.HOME);
232
260
  const tokens = scanTokens({ home, env, procEnv: opts.procEnv || env });
233
261
  const items = [...tokens.items, ...scanGit({ home, env }), ...scanEditor({ home })];
234
- uniqueNames(items);
235
- return { version: 2, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
262
+ return { version: 3, home, platform: opts.platform || process.platform, items, manual: tokens.manual };
236
263
  }
237
264
 
238
265
  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`: those only go through the Vault (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`: their values need explicit Secret import and selection (step 7).
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 (they go through the Vault, 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 (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.
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`; the `instabox-basic-setup` skill, where installed, puts tokens and git config into the Vault).
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).
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 `-`. Every box is the same size. The default template (`universal`) has git, Node.js 22, Python (uv), Go, Rust, a C toolchain and Docker. Do not add a kill timeout: this box is meant to stay (offer `instabox timeout <box> 8h --on-timeout pause` if the user wants it to pause itself). The `repo=` label lets Vault items (step 7) reach this box only.
75
+ Box names are lowercase letters, digits and `-`. Every box is the same size. 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. Do not add a kill timeout: this box is meant to stay (offer `instabox timeout <box> 8h --on-timeout pause` if the user wants it to pause itself). 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.
76
76
 
77
77
  ## 3. Copy the repo and its worktrees
78
78
 
@@ -165,18 +165,28 @@ 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` files: through the Vault, never copied
168
+ ## 7. `.env` values: explicit Secret import and selection
169
169
 
170
- The plan's `envLeftOut` lists them by name (`jq -r '.main.envLeftOut, (.worktrees[] | select(.move) | .envLeftOut) | .[]' "$W/plan.json"`). Do not open them. If the user wants them on the box, the Vault carries them as files under HOME, and a link puts each into the repo. With their go-ahead, per file (the CLI reads the file; nothing passes through you):
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):
171
175
 
172
176
  ```bash
173
- R=$(basename "$DEST")
174
- instabox vault set-file ".config/instabox-move-repo/env/$R/<path in repo>" --from "<local path>" --selector repo="$R"
175
- instabox vault push "$BOX"
176
- instabox exec "$BOX" -- "mkdir -p '$DEST/<dir in repo>' && ln -sfn ~/.config/instabox-move-repo/env/$R/<path in repo> '$DEST/<path in repo>'"
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"
177
183
  ```
178
184
 
179
- The `--selector repo=<name>` keeps the file to boxes created with `--meta repo=<name>` (step 2). For tokens, git config and editor setup across all boxes, point the user to the `instabox-basic-setup` skill.
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.
180
190
 
181
191
  ## 8. Tell the user
182
192
 
@@ -184,7 +194,7 @@ The `--selector repo=<name>` keeps the file to boxes created with `--meta repo=<
184
194
  - 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.
185
195
  - Sizes: on disk before, sent after, and the biggest things left out (they were rebuilt or reinstalled on the box).
186
196
  - 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).
187
- - Again: git push/pull needs credentials set up on the box. 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.
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.
188
198
  - How to pick up old conversations, kept short, with the session ids from step 4:
189
199
  - Claude Code, in the repo (or worktree) directory on the box: `claude --continue` (latest) or `claude --resume` (picker, or `claude --resume <id>`).
190
200
  - Codex, in that directory: `codex resume` (picker for this directory), `codex resume --last`, or `codex resume <id>`.
@@ -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 snapshot includes credentials stored under HOME (gh, Claude, Codex, git, SSH keys), and the Vault's `file` items (its `env` items live on tmpfs and are not saved). Sign out of tools or remove secrets first if the snapshot will be shared or kept long.
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.
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,7 +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 does not inherit `--no-vault`. If the box was created with `--no-vault`, pass it again: `instabox create --name work2 --snapshot work-copy --no-vault`.
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:
54
+
55
+ ```sh
56
+ instabox create --name work2 --snapshot work-copy --no-vault --secrets none --env-secrets none
57
+ ```
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.
54
60
 
55
61
  ## Clean up
56
62