instabox 0.5.4 → 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.
- package/bin/instabox.mjs +45 -3
- package/lib/api.mjs +8 -1
- package/lib/telemetry.mjs +97 -0
- package/package.json +1 -1
- package/skills/instabox/SKILL.md +31 -20
- package/skills/instabox-basic-setup/SKILL.md +57 -57
- package/skills/instabox-basic-setup/scripts/content.mjs +10 -5
- package/skills/instabox-basic-setup/scripts/install-files.py +82 -0
- package/skills/instabox-basic-setup/scripts/lib.mjs +61 -39
- package/skills/instabox-basic-setup/scripts/plan.mjs +248 -230
- package/skills/instabox-basic-setup/scripts/push.mjs +318 -151
- package/skills/instabox-basic-setup/scripts/scan.mjs +73 -46
- package/skills/instabox-move-repo/SKILL.md +22 -12
- package/skills/instabox-snapshots/SKILL.md +8 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Read-only inventory of the basic setup
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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 = [
|
|
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([
|
|
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:
|
|
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
|
|
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 (!
|
|
78
|
-
if (!
|
|
79
|
-
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
117
|
-
(byName.has('ANTHROPIC_API_KEY')
|
|
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
|
|
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
|
|
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('..') &&
|
|
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,
|
|
139
|
-
const e = {
|
|
140
|
-
|
|
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
|
|
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', {
|
|
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 (!
|
|
193
|
-
if (st.size >
|
|
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,
|
|
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 = {
|
|
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:
|
|
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
|
-
|
|
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`:
|
|
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 (
|
|
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`;
|
|
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 (`
|
|
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`
|
|
168
|
+
## 7. `.env` values: explicit Secret import and selection
|
|
169
169
|
|
|
170
|
-
The plan's `envLeftOut` lists
|
|
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
|
-
|
|
174
|
-
instabox
|
|
175
|
-
instabox
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|