Debug sandbox — change nothing, issue nothing
Это содержимое пока не доступно на вашем языке.
Set OPENCODEX_DEBUG_SANDBOX=1 and opencodex runs normally with three things switched off: it will
not write its config to disk, not issue a data-plane key, and not reconfigure the other
tools on your machine. Everything else behaves as it always does — the dashboard renders, settings
toggle, the pairing panel opens, the QR code appears and its countdown runs.
It leaves your other tools alone
Section titled “It leaves your other tools alone”A normal start edits four things that live outside the opencodex data directory entirely:
| What | Where |
|---|---|
| Codex’s provider config | ~/.codex/config.toml |
| Grok Build’s config | ~/.grok/config.toml |
| The shell profile hook | your shell rc file |
| System-wide environment variables | the machine’s environment |
All four are reverted on a clean shutdown, and none of them is reverted by a crash or a force-kill. A sandboxed start declines the whole set rather than any one of them: a half-configured machine — Codex pointed at the proxy, Grok not — is harder to reason about than either end state.
It exists so the app can be driven, demonstrated and screenshotted without changing the machine’s configuration or minting a credential somebody has to remember to revoke.
Turning it on
Section titled “Turning it on”OPENCODEX_DEBUG_SANDBOX=1 ocx startOn Windows, in PowerShell:
$env:OPENCODEX_DEBUG_SANDBOX = "1"; ocx start1, true, yes and on all switch it on, in any case. Anything else — including the empty
string, 0 and false — leaves it off, so exporting the name with no value in a shell profile does
not quietly arm it.
What it blocks
Section titled “What it blocks”| Blocked | Normally | In the sandbox |
|---|---|---|
config.json writes via saveConfig | Every settings change is written | Nothing is written. An existing file is left byte-for-byte alone |
| Pairing | A correct code mints a data-plane key and persists it | The claim is refused with the reason sandbox. No key is minted |
| Minting a data-plane key | mintDataPlaneKey mints on demand — including the one-click enable remote access opt-in | Refused. mintDataPlaneKey throws as a backstop, and each caller checks first and reports honestly |
POST /api/keys | Mints its own ocx_data_… key without going through mintDataPlaneKey | Refused with 409 |
POST /api/host/restore | Rewrites the state files directly, not through saveConfig | Refused with 409, before anything drains — this is the one action that would otherwise really change the machine |
One thing that is not blocked and looks like it should be: a key you supply yourself through the custom-key field is still accepted into the running config. Nothing issues it and nothing writes it, but it is live against this process until it stops.
These two are singled out because they are the two that are awkward to undo:
- Config writes. Toggling Reachable from other devices just to see what the screen does
rewrites
config.json, and on the next start the proxy really is published to your network. There was previously no way to look at that screen in its enabled state without actually enabling it. - Pairing. A key minted to take a screenshot is a live credential that outlives the screenshot, and whoever made it is the least likely to remember to revoke it.
Because the config write is what publishes the proxy, blocking it also means the sandbox cannot publish the proxy to your network by accident.
How you can tell it is on
Section titled “How you can tell it is on”A mode that silently stops settings saving is indistinguishable from a bug, and “my settings do not save” is the kind of thing that gets reported as data loss. So it says so:
- Remote access & backup shows a banner, first in the card and above the toggle it explains.
The settings search cannot hide it — it is not behind a
matches(...)gate, and it keeps the card itself on screen even when a query filters every row out. GET /api/hostreportsdebugSandbox: true, which is what the banner reads. Anything else built on that endpoint gets the same signal for free.- The proxy writes one line to stderr, the first time it blocks anything:
[debug-sandbox] OPENCODEX_DEBUG_SANDBOX is set: config changes are NOT written to diskand no data-plane key will be issued. Other files (logs, usage, state) are still writtenas normal — set OPENCODEX_HOME to a throwaway directory if you need a clean slate.
A phone that scans a code against a sandboxed desktop is told plainly too, rather than being left to guess: “The desktop is running in debug mode… Scanning again will not help.”
What it deliberately does not do
Section titled “What it deliberately does not do”It is not a security boundary
Section titled “It is not a security boundary”Never describe it as one. It is a convenience for the person driving the app, and it lives inside the process it is protecting — anything already able to set an environment variable on this process could equally unset it. The real boundary is the data-plane credential; the management plane is intentionally open, and the sandbox does not change that or the pairing-token flow.
It does not fake success
Section titled “It does not fake success”A blocked pairing claim is refused, with a reason of its own, rather than answered with a fabricated key. A phone told it had paired would fail on every request afterwards with no clue why — a worse debugging experience than the one this exists to improve.
It does not change how a wrong code is answered
Section titled “It does not change how a wrong code is answered”A wrong or expired code gets exactly the answer it gets outside the sandbox — mismatch,
expired, no-pairing. Only a caller presenting the correct live code ever sees sandbox.
That is on purpose. If the sandbox answered sandbox to any old guess, the refusal would depend on
nothing but the mode, and POST /api/host/pair/claim — the one route that answers without a
credential — would become a way for anyone on the network to ask whether your desktop is in debug
mode. It is pinned by a test.
It does not consume the code it refuses
Section titled “It does not consume the code it refuses”Nothing was issued, so there is nothing to spend. The same code keeps working, and leaving the sandbox lets you pair with it for real without generating another.
How the enabled state is shown without enabling anything
Section titled “How the enabled state is shown without enabling anything”Turning Reachable from other devices on inside the sandbox does not set the hostname, even in
memory. The requested bind is recorded for display only, and describeHost renders from it; the live
config, the auth posture and the listening socket are all untouched.
That indirection is not neatness — the direct version was a trap. isApiAuthRequired is derived from
config.hostname, so setting it still makes the running process demand a data-plane credential for
/v1/*, while management routes remain open. The result used to be a process no credential could
satisfy: an unauthenticated GET /v1/models answered 200 before the toggle and 401 after, and
so did one carrying the admin token. That is exactly the unreachable state
assertServerAuthConfig exists to prevent at startup, reached at runtime instead.
A test pins it: after the toggle the data plane answers exactly as it did before, and the persisted hostname is still loopback.
Failure modes
Section titled “Failure modes”| Symptom | Cause |
|---|---|
| Settings spring back after a restart | Working as intended. The banner on Remote access says so |
| The phone says the desktop is in debug mode | The desktop has the variable set. Restart it without one |
| The variable is set but everything still saves | The value is not one of 1 / true / yes / on |
| No banner, but nothing saves | Something else — the banner and the block read the same flag, so they cannot disagree. Check GET /api/host |
Files still appearing under OPENCODEX_HOME | Expected. Only config writes and key issuance are blocked; see the caution at the top |
Verification
Section titled “Verification”tests/debug-sandbox.test.ts — 17 tests. The flag’s accepted and rejected spellings; that
saveConfig creates no config file, leaves an existing one unchanged byte-for-byte, and that a
re-read still returns the original; that the announcement fires exactly once and names the variable;
that a correct code is refused with no key minted; that the refused code survives to pair for real
once the sandbox is off; that a wrong code still answers mismatch and an absent pairing still
answers no-pairing; that mintDataPlaneKey throws its backstop; that PUT /api/host with either
mintKeyIfMissing or newKeyName returns no key while still reaching the exposed state; that
the same request outside the sandbox still mints, so the fix did not quietly disable the feature;
and that describeHost reports the flag.
The key-minting cases exist because the first version of this mode did hand out a live
ocx_… key from the one-click enable remote access path, captioned “shown once, store it now”.
It was found by running the built app and looking at the screen, not by reading the code — which is
why the tests now drive the real route over a real socket rather than calling the function.
Suggested reading
Section titled “Suggested reading”- Remote access and pairing a phone — the flow this mode lets you exercise safely
- The web dashboard — the two-credential split the sandbox does not alter
- Log files — the files the sandbox does not stop being written

