This guide covers the supported ChatGPT-web-only DevSpace architecture.
The repository-local command is always:
node dist/cli.js --helpThe shorter devspace command requires:
npm linkRun both from the DevSpace source checkout.
DevSpace requires Node.js >=22.19 <27. Check:
node --version
npm --versionUse the same Node installation for npm ci, npm run build, and the running
service.
This usually means dependencies were installed under a different Node ABI.
npm ci
npm run build
node dist/cli.js doctorIf a service manager starts DevSpace, verify that it uses the same absolute Node binary as the successful build.
Store only the public origin:
https://devspace.example.com
Use the origin plus /mcp only as the ChatGPT app endpoint:
https://devspace.example.com/mcp
Fix an incorrect value with:
node dist/cli.js config set publicBaseUrl https://devspace.example.comThen restart DevSpace and update the ChatGPT app.
Random quick-tunnel hostnames are temporary. After a change:
- update
publicBaseUrl; - restart DevSpace;
- confirm local and public
/readyz; - update the ChatGPT app endpoint;
- approve a new OAuth grant.
A stable hostname avoids repeating this procedure.
Verify:
DEVSPACE_PUBLIC_BASE_URLmatches the browser-visible HTTPS origin;- the reverse proxy preserves an expected
Hostheader; DEVSPACE_ALLOWED_HOSTS, if set, includes the actual host;- only the public service port is tunneled.
Do not proxy the loopback admin/control port.
The redirect URI must use an allowed host. Configure
DEVSPACE_OAUTH_ALLOWED_REDIRECT_HOSTS only for hosts you trust. Do not add a
broad wildcard to make an unknown redirect pass.
The stored verifier cannot recover the plaintext Owner password. Check for:
- transcription errors;
- a different
DEVSPACE_CONFIG_DIR; - an environment override in
DEVSPACE_OAUTH_OWNER_TOKEN; - a service manager using different environment settings.
Do not paste the Owner password into ChatGPT or a repository file.
Approving connection B does not replace connection A. DevSpace supports multiple concurrently active grants, including several grants for the same OAuth client.
If A stopped working, check whether A's exact grant was revoked or expired, refresh-token replay invalidated it, its authorization epoch changed, or its approved Project was removed. Reauthorize A without revoking B. If separate people or trust domains require OS-level isolation, run separate DevSpace instances under separate OS users because grants do not sandbox shared local files or commands.
More than one Project is approved, so project_control(action=open) cannot use
a default. Call list_projects, let the user choose, then retry creation with
its returned projectRef and the intended operationId.
With exactly one approved Project, call project_control(action=open) directly
with operationId; listing first is unnecessary.
No usable execution is selected for the trusted ChatGPT session and Actor, or
the saved binding is stale. Call project_control(action=open) for a new task,
or call list_projects, select an explicit tasks[].taskRef, and call
project_control(action=resume). Do not send an absolute path, internal
identifier, or infer a recent or sole Project. After a stable-session reconnect,
try project_control(action=hydrate); if it reports the binding is missing or
stale, reselect explicitly.
The approved shared Project path cannot be recovered or no longer matches the
persisted Project identity. Restore the directory or approve the intended
Project again, then resume. Create another logical context with a new
operationId only when that is the intended action. DevSpace does not repair or
change Git state.
The active OAuth grant lacks the capability required by the tool. The public scopes are:
project:readfor selection, instructions, Skills, reads, inspection, and change review;project:writefor patches;process:executefor commands and process interaction.
Approve a new grant with the required capabilities. Other active grants remain valid.
DEVSPACE_ALLOWED_ROOTS is the service-wide ceiling. OAuth approval separately
selects Projects within that ceiling.
After adding a root:
- verify the root in the local admin panel or
config get; - call
list_projects; - if the Project is not authorized, approve a new grant and select it;
- call
project_control(action=open)directly for one Project, orlist_projects → project_control(action=open)when several are approved.
Never approve a broad parent directory merely to make discovery easier.
File paths and command workingDirectory values must resolve inside the referenced
Project.
Common causes are:
- an absolute path instead of a Project-relative path;
..traversal outside the checkout;- a symlink whose canonical target is outside the approved root;
- a
workingDirectorythat names a file or missing directory; - the root was removed after the Project was selected.
Use a path relative to the selected Project. Do not weaken the approved-root configuration to accommodate an unrelated path.
Project open, resume, and hydrate return compact bounded root instruction
pages. read_files and inspect return a newly applicable nested
instructionsDelta with the
target result. A mutation or command may instead return
instructions_required and start no effect; review that delta before retrying.
Check:
- the instruction file is inside the approved checkout;
- the filename and nesting match the repository convention;
AGENTS.override.mdorAGENTS.mdis used by default;CLAUDE.mdis listed inprojectDocFallbackFilenamesorDEVSPACE_PROJECT_DOC_FALLBACK_FILENAMESif that explicit fallback is intended;- the file fits the configured instruction limits;
DEVSPACE_USER_INSTRUCTIONS_PATH, if set, points to the intended file;- the current trusted session+Actor selected the intended Project with
project_control.
Check:
- the Skill has a readable
SKILL.md; - required manifest fields are present;
- the directory is in a discovered or explicitly configured Skill root;
- it is not listed in
DEVSPACE_DISABLED_SKILL_PATHS.
Use skills with a query for explicit discovery. Then call it with a returned
skillId before following the selected instructions. Skill bodies are
intentionally lazy.
The file changed after ChatGPT read it. This is a safety check, not a transient write failure.
Recovery:
- read the current file;
- reconcile the intended edit with the new content;
- create a new patch using the current version;
- review with
show_changesand an explicitsource.
Do not remove the precondition or blindly overwrite the newer file.
Effectful calls use operation replay protection. Reusing an operation identifier with a different request body is rejected.
- Retry an identical lost-response request with the same identifier.
- For a saved-Task revision conflict from
save_progress, first calllist_projects(projectRef)and reconcile the latest Task, then retry with the sameoperationIdand currentifMatch; that rejected attempt did not start an effect. - Use a new identifier for a logically new effect.
- If the server reports an uncertain outcome, inspect files or process output before deciding what to do next.
Another DevSpace write or tracked command holds the root lock. An interactive or background command may keep the lease until its process tree exits or is cleaned up.
Wait or finish the owning process. Do not bypass the lock by running a second DevSpace instance against the same checkout. External editors are not covered by this lock, so file-version checks still matter.
That is the documented security boundary. DevSpace validates the declared
workingDirectory, but it does not sandbox the child process. The command has the
authority of the OS user running DevSpace and can use absolute paths and the
network.
Use a dedicated low-privilege OS user, container, or VM when stronger isolation
is required. Treat process:execute as high-trust access.
Direct exec_command uses program and args; explicit shell mode uses
shell:true and command. Program names and shell syntax
differ across operating systems. Use values valid for the OS running DevSpace
and set workingDirectory separately.
Do not assume a Unix shell is present on Windows.
DevSpace applies fixed bounded inline-output and wait budgets. A long-running command may return a process handle instead of waiting for completion.
Use:
read_process_outputwithsessionIdto poll a live process;write_stdinwith a freshoperationIdonly when input, close, or interrupt is needed;read_process_outputwithoutputIdfor the first retained-output read;- a narrower test or log filter when output is too large.
Retained output is size- and time-limited. It is not permanent storage.
The process may have exited, expired, been cleaned up, or belong to an inactive
authorization or Project context. Inspect the original exec_command result and
try read_process_output if retained output is still available.
Do not guess process identifiers from another execution. An empty
write_stdin call is not a poll; it is rejected because the tool is
mutation-only.
Signed cursors are self-contained and bound to the active grant, Project
generation, resource revision, query, and paging parameters. On continuation,
use the same session+Actor selection and pass only the cursor. Do not repeat or
change the original outputId, mode, query, or offset beside it.
For show_changes, the first call supplies source; continuation supplies only
the cursor. Repeating either source beside the cursor is rejected. If that
cursor is invalid, expired, or stale, restart with an explicit source and no
cursor. Use the returned recovery source when one is available.
For other resources, if the resource or Project changed, restart the read without the stale cursor.
Confirm:
- the trusted session+Actor selected the Project context you intended;
sourceis explicitly set torepositoryorapply_patch_history;- the edit succeeded rather than failing an
ifMatchcheck; - the execution is still active under the current grant.
With source:"repository", confirm that the Project root is the exact Git top
level and that the change is visible to the current repository diff. Nested and
non-Git Projects reject this source. source:"apply_patch_history" includes only
the exact successful DevSpace apply_patch requests recorded under that execution;
it excludes command writes, external edits, and patches from other executions,
even though those files are visible in the shared directory. It is a bounded
chronological operation log, not a net filesystem diff. If it is full, create a
new logical context for the same Project.
show_changes is read-only and bounded. A very large result returns an optional
next cursor; continue with that cursor alone.
read_files and inspect bound both input and output. Large requests may omit
bodies or return a summary.
Split the request into smaller, known file sets. Avoid repeatedly scanning the whole repository.
Before restarting:
- note any running command;
- stop or finish interactive work when practical;
- restart only the DevSpace service;
- confirm local and public
/readyz; - in the same stable host session, call
project_control({"action":"hydrate"}); if the binding is unavailable, explicitly open or resume the intended Project.
Restarting may clean up running or retained process state, but persisted executions are revalidated when their trusted session bindings are used again. DevSpace termination covers only process groups it started and still tracks, on a best-effort basis; detached or untracked descendants may survive. Restart does not delete Project files or change Git state.
That is deliberate. Executions isolate opaque references, authorization, instruction state, idempotency, processes, and non-Git patch journals; they do not isolate the filesystem. All executions bound to the same approved Project use that existing directory.
Ask the model to use ordinary Git branches or worktrees when isolation is needed. DevSpace itself never creates, removes, resets, or prunes them.
See ChatGPT Tool Contract for the canonical surface and recovery behavior.