Skip to content

Security: keepkeen/devspace

Security

docs/security.md

Security Model

DevSpace is a single-owner bridge between ChatGPT web and approved local Projects. Its primary controls are narrow Project approval, OAuth capabilities, grant-bound execution references, path and version validation, effect replay protection, bounded Project handoffs, bounded output, and root locking.

It is not an operating-system security boundary.

Trust model

The trusted operator is the person who:

  • runs DevSpace under a local OS account;
  • chooses the approved Project roots;
  • controls the Owner password and persistent state;
  • reviews and approves the ChatGPT OAuth grant;
  • decides which commands ChatGPT may run.

Repository files, AGENTS.md, Skills, build scripts, command output, saved handoff text, and model text are untrusted input. They cannot grant capabilities or expand an approved root.

Approved roots and Projects

Configure the narrowest roots that contain the checkouts ChatGPT needs. Avoid approving:

  • / or a full home directory;
  • a cloud-drive root;
  • a secrets directory;
  • a parent containing unrelated work and personal data.

list_projects exposes only Projects selected for the active OAuth grant and does not need to reveal absolute local paths. use_project validates the returned Project reference, binds a logical context to that approved directory, and returns an opaque executionRef. Git is not required.

File tools resolve Project-relative paths and verify that canonical targets remain inside the approved root. Command workdir receives the same containment check. These checks prevent accidental or model-supplied path traversal through the DevSpace tool arguments.

They do not constrain what an already-started local process can access.

Project execution binding

The OAuth bearer grant is the request identity. DevSpace does not consume or persist ChatGPT account or conversation identifiers.

Creating an execution requires a caller-chosen operationId and, when more than one Project is approved, a projectRef. The result is an HMAC-authenticated executionRef. Every Project-scoped tool requires that reference explicitly. Before the tool handler runs, DevSpace verifies that the execution still belongs to the active principal, OAuth client, grant, authorization epoch, approved Project, and expected shared directory.

The reference is durable across MCP transports, ChatGPT conversations, and service restarts. It is not reusable under any different grant, including a second grant issued to the same OAuth client. Removing the source Project from authorization closes the execution before a new effect can start.

Each new operation creates a different logical context on the same approved directory. Retrying the identical use_project request replays the same execution, which makes a lost response safe without creating another context. DevSpace never selects an execution by recency and has no process-global “current Project.”

Logical contexts isolate references, instruction state, idempotency records, process handles, patch journals, and grant authorization—not files. Any two contexts or grants approved for the same Project see the same filesystem and can observe one another's writes. DevSpace root locks coordinate tracked DevSpace writers, but cannot serialize external programs or edits outside DevSpace.

Project handoffs

A Project handoff is a bounded semantic progress snapshot for continuing work in a later ChatGPT conversation. It is not a ChatGPT account/session record, chat transcript, command log, diff, file snapshot, or source of authorization.

Handoffs are keyed by the approved Project's stable fingerprint rather than by an OAuth grant. Any active grant that currently authorizes that same Project may list and continue its resumable handoffs. Continuing one always creates a new execution bound to the calling grant; it never transfers or reuses another grant's executionRef, process handles, instruction acknowledgement, mutation replay records, or non-Git patch journal. Knowing a handoffRef alone cannot bypass Project authorization.

Each handoff has a title of at most 256 UTF-8 bytes and progress text of at most 8 KiB. Their JSON-serialized model text must also fit 12,000 bytes. A Project may have at most 20 resumable handoffs and retains at most the newest 80 completed records; selection output is also bounded. save_progress uses a caller-stable operationId and an integer ifMatch revision for replay-safe, optimistic updates. Save responses and Project lists omit the progress body to avoid duplicating it in model context.

Do not place secrets, credentials, hidden reasoning, complete file contents, full diffs, transcripts, or raw logs in a handoff. Handoff text is durable state and should be treated as sensitive. On resume, DevSpace returns it only on the first Project-context page with explicit untrusted historical provenance and mustRevalidate: true; the model must reread relevant files and Git state before acting on it. A completed handoff is removed from resume selection but its bounded record remains until it ages out of the per-Project completed retention set. Pruning replaces obsolete execution links with a terminal marker so those executions cannot create a new handoff; it removes metadata only, never Project files.

Owner and OAuth

DevSpace has one hidden local Owner. The Owner password gates the OAuth approval page and is stored as a verifier rather than recoverable plaintext. Keep the password and auth.json out of repositories, chats, screenshots, and logs.

Multiple OAuth grants may remain active concurrently, including grants sharing the same OAuth client ID. Each access token resolves an exact principal/client/grant/authorization-epoch boundary with its own scopes and approved Projects. Issuing a new grant does not invalidate another grant's tokens, executions, transports, or Project selection.

Revoking or expiring one grant affects only that grant. Its tracked executions and processes are closed through the durable cleanup path, retained process output and review state are retired, and Project files and Git state are left untouched. Refresh-token replay revokes only the replayed grant. Owner-password rotation and the authenticated revokeAll operation remain intentional global emergency actions.

DevSpace does not claim to identify a ChatGPT account: it stores no ChatGPT account or conversation key. Independent OAuth grants are the multi-user security boundaries, even when the host reuses one OAuth client ID. Executions and processes remain grant-local. Project handoffs are the deliberate exception: they are shared only among grants that currently authorize the same Project, as described above.

The public OAuth scopes are fixed:

Scope Authority
project:read Select approved Projects; load and save Project handoffs; load instructions and Skills; read, inspect, and review changes.
project:write Apply file patches.
process:execute Explicit high-trust opt-in for process I/O and, together with project:write, command creation.

Every tool call rechecks the active grant and the capability required by that tool. A projectRef is a selector, not a credential.

Access and refresh tokens are bearer credentials. Protect them in transit with HTTPS, never log them, and do not place them in shell history or repository content.

Public and local listeners

DEVSPACE_PUBLIC_BASE_URL is the public HTTPS origin, without /mcp. The MCP endpoint is that origin plus /mcp.

The public listener serves the MCP and OAuth routes plus health endpoints. Administrative and internal control routes remain on loopback-only listeners. Tunnel or reverse-proxy only the public DevSpace service port. Never expose the admin/control listener.

Host-header and OAuth redirect-host validation reduce endpoint confusion and redirect abuse. They are configuration checks, not a replacement for TLS or OAuth.

Temporary tunnel URLs change. When one changes, update the public origin, restart the server, update the ChatGPT app endpoint, and authorize again.

File mutation safety

DevSpace file writes preserve several invariants:

  • Project-relative and canonical-path containment;
  • ifMatch file-version preconditions;
  • operationId replay protection where required by the tool contract;
  • per-root coordination for conflicting writes;
  • bounded request, response, and diff data.

An ifMatch failure means the file changed after it was read. The caller must reread and reconcile the edit.

Reusing the same operation identifier for the same request after a lost response replays the stored result rather than repeating the effect. A different intended effect requires a new identifier.

Root locks coordinate DevSpace writers that target the same checkout. A running process may retain its root lease until the tracked process tree exits or is cleaned up. Locks prevent cooperating DevSpace operations from racing; they do not stop unrelated local programs from editing the same files. save_progress updates only DevSpace metadata and does not acquire the Project root lock, so a checkpoint can be recorded while a tracked command still holds the filesystem lease.

Signed continuation cursors bind the grant, authorization epoch, execution, resource, revision, query, and paging parameters. Continue with the same executionRef plus the cursor and omit the initial paging fields. A cursor is not Project authority and cannot be transferred to another grant or execution.

Command execution

exec_command is intentionally a full local command facility. It accepts:

executionRef, operationId, cmd, workdir, env, yield_time_ms,
max_output_tokens, tty

DevSpace validates that workdir is inside the Project bound to the execution and bounds returned output. write_stdin is mutation-only and requires operationId to send input, close stdin, interrupt, or resize a terminal. read_process_output performs live polling and retained-output reads without mutating process input. Process count, retention, and cleanup limits prevent unbounded server-side accumulation.

After process creation, the OS is the enforcement boundary. The command runs with the privileges of the OS user running DevSpace and can:

  • access any file that OS user can access, including paths outside the Project;
  • use the network according to the host OS and environment;
  • spawn subprocesses and execute repository-provided scripts;
  • make changes that DevSpace file-version checks cannot observe in advance.

DevSpace provides no process sandbox, command allow/deny list, risk classification, child-process protected-path policy, or network egress policy. Project and workdir validation must not be described as shell isolation.

Shutdown and interrupt cover only process groups that DevSpace started and still tracks, and termination is best effort. Detached, daemonized, re-parented, or otherwise untracked descendants may outlive DevSpace.

For stronger isolation, run DevSpace with a dedicated low-privilege OS user or inside a suitably configured container or VM. Treat process:execute as high-trust authority.

Instructions and Skills

use_project returns a compact bounded root instruction delta without an eager Skill catalog. read_files and inspect return newly applicable nested instructionsDelta only when target paths require it. The skills tool searches bounded metadata and lazily loads one selected Skill.

Default repository instruction discovery uses AGENTS.override.md and AGENTS.md (including supported case variants). CLAUDE.md is not loaded unless the operator explicitly configures it as a fallback filename.

These sources may describe how to work, but they cannot:

  • alter OAuth capabilities or approved roots;
  • authorize another local path;
  • disable file-version or replay checks;
  • disclose server credentials that were not already present in accessible content.

Review repository-provided instructions and Skills with the same care as build scripts.

Shared Project directories and change review

The approved Project root is the mutable execution directory. use_project accepts any existing approved directory and never creates, switches, removes, or validates Git branches or worktrees. Users may ask the model to manage Git through ordinary commands, subject to the full command-security boundary above.

When the approved Project root is exactly the Git top level, show_changes reads the current staged, unstaged, and untracked repository diff without writing the index, objects, or refs. A Project nested inside a larger repository uses the non-Git source so review cannot expose paths above the approved root. This includes repositories with no first commit. DevSpace disables Git fsmonitor for review and rejects executable clean/process filters that apply to Project files, because a project:read tool must not run repository-configured programs. The non-Git source is a bounded durable journal containing the exact successful DevSpace apply_patch requests for the current logical execution. It is not a filesystem monitor or net diff: command writes, external edits, failed or unknown-outcome patches, and patches from another execution are excluded. A full journal requires starting a new logical context; shared files are not reset or copied.

Closing or revoking an authorization retires its logical and retained runtime state without deleting Project files or changing Git state. There is no worktree inventory or cleanup API.

Output and process retention

Read results, diffs, command output, and retained process output are bounded. Truncation is expected for large data; callers should narrow their request.

Running and completed process records are subject to configured limits and cleanup. Do not use retained output as permanent storage. Project handoffs are also bounded but intentionally durable; they are continuity metadata, not a backup of Project files or chat history.

Logs and secrets

Keep the following out of logs and repositories:

  • Owner passwords and OAuth tokens;
  • auth.json and master-key material;
  • internal control tokens;
  • tunnel credentials;
  • command text or output that contains secrets.

Use DEVSPACE_LOG_SHELL_COMMANDS=1 only when command previews are intentionally acceptable. Audit and diagnostic output should remain bounded and sanitized, but operators must still review it before sharing.

GET /healthz is a liveness check. GET /readyz is a readiness check and should not expose credentials or local file contents.

The canonical public surface and examples are in ChatGPT Tool Contract.

There aren't any published security advisories